Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
overtake 2020-06-19 16:58:29 +03:00
commit 74a4b18a17
49 changed files with 5078 additions and 4342 deletions

View File

@ -3,7 +3,7 @@
include Utils.makefile include Utils.makefile
APP_VERSION="6.2.1" APP_VERSION="6.3"
CORE_COUNT=$(shell sysctl -n hw.logicalcpu) CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1) CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1)

View File

@ -5565,6 +5565,7 @@ Any member of this group will be able to see messages in the channel.";
"Stats.GroupMessagesTitle" = "MESSAGES"; "Stats.GroupMessagesTitle" = "MESSAGES";
"Stats.GroupActionsTitle" = "ACTIONS"; "Stats.GroupActionsTitle" = "ACTIONS";
"Stats.GroupTopHoursTitle" = "TOP HOURS"; "Stats.GroupTopHoursTitle" = "TOP HOURS";
"Stats.GroupTopWeekdaysTitle" = "TOP DAYS OF WEEK";
"Stats.GroupTopPostersTitle" = "TOP MEMBERS"; "Stats.GroupTopPostersTitle" = "TOP MEMBERS";
"Stats.GroupTopAdminsTitle" = "TOP ADMINS"; "Stats.GroupTopAdminsTitle" = "TOP ADMINS";
"Stats.GroupTopInvitersTitle" = "TOP INVITERS"; "Stats.GroupTopInvitersTitle" = "TOP INVITERS";
@ -5583,6 +5584,9 @@ Any member of this group will be able to see messages in the channel.";
"Stats.GroupTopPosterChars_many" = "%@ symbols per message"; "Stats.GroupTopPosterChars_many" = "%@ symbols per message";
"Stats.GroupTopPosterChars_any" = "%@ symbols per message"; "Stats.GroupTopPosterChars_any" = "%@ symbols per message";
"Stats.GroupTopPoster.History" = "History";
"Stats.GroupTopPoster.Promote" = "Promote";
"Stats.GroupTopAdminDeletions_0" = "%@ deletions"; "Stats.GroupTopAdminDeletions_0" = "%@ deletions";
"Stats.GroupTopAdminDeletions_1" = "%@ deletion"; "Stats.GroupTopAdminDeletions_1" = "%@ deletion";
"Stats.GroupTopAdminDeletions_2" = "%@ deletions"; "Stats.GroupTopAdminDeletions_2" = "%@ deletions";
@ -5604,9 +5608,15 @@ Any member of this group will be able to see messages in the channel.";
"Stats.GroupTopAdminBans_many" = "%@ bans"; "Stats.GroupTopAdminBans_many" = "%@ bans";
"Stats.GroupTopAdminBans_any" = "%@ bans"; "Stats.GroupTopAdminBans_any" = "%@ bans";
"Stats.GroupTopAdmin.Actions" = "Actions";
"Stats.GroupTopAdmin.Promote" = "Promote";
"Stats.GroupTopInviterInvites_0" = "%@ invitations"; "Stats.GroupTopInviterInvites_0" = "%@ invitations";
"Stats.GroupTopInviterInvites_1" = "%@ invitation"; "Stats.GroupTopInviterInvites_1" = "%@ invitation";
"Stats.GroupTopInviterInvites_2" = "%@ invitations"; "Stats.GroupTopInviterInvites_2" = "%@ invitations";
"Stats.GroupTopInviterInvites_3_10" = "%@ invitations"; "Stats.GroupTopInviterInvites_3_10" = "%@ invitations";
"Stats.GroupTopInviterInvites_many" = "%@ invitations"; "Stats.GroupTopInviterInvites_many" = "%@ invitations";
"Stats.GroupTopInviterInvites_any" = "%@ invitations"; "Stats.GroupTopInviterInvites_any" = "%@ invitations";
"Stats.GroupTopInviter.History" = "History";
"Stats.GroupTopInviter.Promote" = "Promote";

View File

@ -214,6 +214,34 @@ public final class ChatPeerNearbyData: Equatable {
} }
} }
public enum ChatSearchDomain: Equatable {
case everything
case members
case member(Peer)
public static func ==(lhs: ChatSearchDomain, rhs: ChatSearchDomain) -> Bool {
switch lhs {
case .everything:
if case .everything = rhs {
return true
} else {
return false
}
case .members:
if case .members = rhs {
return true
} else {
return false
}
case let .member(lhsPeer):
if case let .member(rhsPeer) = rhs, lhsPeer.isEqual(rhsPeer) {
return true
} else {
return false
}
}
}
}
public final class NavigateToChatControllerParams { public final class NavigateToChatControllerParams {
public let navigationController: NavigationController public let navigationController: NavigationController
public let chatController: ChatController? public let chatController: ChatController?
@ -227,7 +255,7 @@ public final class NavigateToChatControllerParams {
public let useExisting: Bool public let useExisting: Bool
public let purposefulAction: (() -> Void)? public let purposefulAction: (() -> Void)?
public let scrollToEndIfExists: Bool public let scrollToEndIfExists: Bool
public let activateMessageSearch: Bool public let activateMessageSearch: (ChatSearchDomain, String)?
public let peekData: ChatPeekTimeout? public let peekData: ChatPeekTimeout?
public let peerNearbyData: ChatPeerNearbyData? public let peerNearbyData: ChatPeerNearbyData?
public let animated: Bool public let animated: Bool
@ -235,7 +263,7 @@ public final class NavigateToChatControllerParams {
public let parentGroupId: PeerGroupId? public let parentGroupId: PeerGroupId?
public let completion: (ChatController) -> Void public let completion: (ChatController) -> Void
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: Bool = false, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
self.navigationController = navigationController self.navigationController = navigationController
self.chatController = chatController self.chatController = chatController
self.context = context self.context = context
@ -482,6 +510,7 @@ public protocol SharedAccountContext: class {
func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError> func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError>
func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController
func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController? func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController?
func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController?
func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController
func makePeersNearbyController(context: AccountContext) -> ViewController func makePeersNearbyController(context: AccountContext) -> ViewController
func makeComposeController(context: AccountContext) -> ViewController func makeComposeController(context: AccountContext) -> ViewController

View File

@ -11,15 +11,31 @@ public enum RequestCallResult {
case alreadyInProgress(PeerId) case alreadyInProgress(PeerId)
} }
public enum PresentationCallState: Equatable { public struct PresentationCallState: Equatable {
case waiting public enum State: Equatable {
case ringing case waiting
case requesting(Bool) case ringing
case connecting(Data?) case requesting(Bool)
case active(Double, Int32?, Data) case connecting(Data?)
case reconnecting(Double, Int32?, Data) case active(Double, Int32?, Data)
case terminating case reconnecting(Double, Int32?, Data)
case terminated(CallId?, CallSessionTerminationReason?, Bool) case terminating
case terminated(CallId?, CallSessionTerminationReason?, Bool)
}
public enum VideoState: Equatable {
case notAvailable
case available(Bool)
case active
}
public var state: State
public var videoState: VideoState
public init(state: State, videoState: VideoState) {
self.state = state
self.videoState = videoState
}
} }
public protocol PresentationCall: class { public protocol PresentationCall: class {
@ -44,6 +60,8 @@ public protocol PresentationCall: class {
func toggleIsMuted() func toggleIsMuted()
func setIsMuted(_ value: Bool) func setIsMuted(_ value: Bool)
func setEnableVideo(_ value: Bool)
func switchVideoCamera()
func setCurrentAudioOutput(_ output: AudioSessionOutput) func setCurrentAudioOutput(_ output: AudioSessionOutput)
func debugInfo() -> Signal<(String, String), NoError> func debugInfo() -> Signal<(String, String), NoError>

View File

@ -666,6 +666,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
case .destructive: case .destructive:
color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor
case .accent:
color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor
} }
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor))
index += 1 index += 1

View File

@ -30,6 +30,12 @@ public class PercentPieChartController: BaseChartController {
let pieController: PieChartComponentController let pieController: PieChartComponentController
let transitionRenderer: PercentPieAnimationRenderer let transitionRenderer: PercentPieAnimationRenderer
var initiallyZoomed = false
public convenience init(chartsCollection: ChartsCollection, initiallyZoomed: Bool) {
self.init(chartsCollection: chartsCollection)
self.initiallyZoomed = initiallyZoomed
}
override public init(chartsCollection: ChartsCollection) { override public init(chartsCollection: ChartsCollection) {
transitionRenderer = PercentPieAnimationRenderer() transitionRenderer = PercentPieAnimationRenderer()
percentController = PercentChartComponentController(isZoomed: false, percentController = PercentChartComponentController(isZoomed: false,
@ -88,14 +94,14 @@ public class PercentPieChartController: BaseChartController {
pieController.previewBarChartRenderer] pieController.previewBarChartRenderer]
} }
public override func initializeChart() { public override func initializeChart() {
percentController.initialize(chartsCollection: initialChartsCollection, percentController.initialize(chartsCollection: initialChartsCollection,
initialDate: Date(), initialDate: Date(),
totalHorizontalRange: BaseConstants.defaultRange, totalHorizontalRange: BaseConstants.defaultRange,
totalVerticalRange: BaseConstants.defaultRange) totalVerticalRange: BaseConstants.defaultRange)
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false) switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
if let lastDate = initialChartsCollection.axisValues.last { if let lastDate = initialChartsCollection.axisValues.last, self.initiallyZoomed {
TimeInterval.animationDurationMultipler = 0.00001 TimeInterval.animationDurationMultipler = 0.00001
self.didTapZoomIn(date: lastDate, animated: false) self.didTapZoomIn(date: lastDate, animated: false)
TimeInterval.animationDurationMultipler = 1.0 TimeInterval.animationDurationMultipler = 1.0

View File

@ -11,6 +11,7 @@ public enum ChartType {
case lines case lines
case twoAxis case twoAxis
case pie case pie
case area
case bars case bars
case step case step
case twoAxisStep case twoAxisStep
@ -76,7 +77,9 @@ public func createChartController(_ data: String, type: ChartType, getDetailsDat
controller = TwoAxisLinesChartController(chartsCollection: collection) controller = TwoAxisLinesChartController(chartsCollection: collection)
controller.isZoomable = false controller.isZoomable = false
case .pie: case .pie:
controller = PercentPieChartController(chartsCollection: collection) controller = PercentPieChartController(chartsCollection: collection, initiallyZoomed: true)
case .area:
controller = PercentPieChartController(chartsCollection: collection, initiallyZoomed: false)
case .bars: case .bars:
controller = StackedBarsChartController(chartsCollection: collection) controller = StackedBarsChartController(chartsCollection: collection)
controller.isZoomable = false controller.isZoomable = false

View File

@ -274,6 +274,7 @@ public enum ItemListPeerItemRevealOptionType {
case neutral case neutral
case warning case warning
case destructive case destructive
case accent
} }
public struct ItemListPeerItemRevealOption { public struct ItemListPeerItemRevealOption {
@ -624,6 +625,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
case .destructive: case .destructive:
color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor
case .accent:
color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor
} }
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor))
index += 1 index += 1

View File

@ -21,7 +21,7 @@
@property (nonatomic, copy) void (^micLevel)(CGFloat level); @property (nonatomic, copy) void (^micLevel)(CGFloat level);
@property (nonatomic, copy) void (^onDuration)(NSTimeInterval duration); @property (nonatomic, copy) void (^onDuration)(NSTimeInterval duration);
@property (nonatomic, copy) void(^finishedWithVideo)(NSURL *videoURL, UIImage *previewImage, NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, id liveUploadData, TGVideoEditAdjustments *adjustments, bool, int32_t); @property (nonatomic, copy) void(^finishedWithVideo)(NSURL *videoURL, UIImage *previewImage, NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, id liveUploadData, TGVideoEditAdjustments *adjustments, bool, int32_t);
@property (nonatomic, copy) void(^onDismiss)(bool isAuto); @property (nonatomic, copy) void(^onDismiss)(bool isAuto, bool isCancelled);
@property (nonatomic, copy) void(^onStop)(void); @property (nonatomic, copy) void(^onStop)(void);
@property (nonatomic, copy) void(^onCancel)(void); @property (nonatomic, copy) void(^onCancel)(void);
@property (nonatomic, copy) void(^didDismiss)(void); @property (nonatomic, copy) void(^didDismiss)(void);

View File

@ -457,6 +457,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
_lockPanelWrapperView.transform = CGAffineTransformMakeTranslation(0.0f, 100.0f); _lockPanelWrapperView.transform = CGAffineTransformMakeTranslation(0.0f, 100.0f);
_lockPanelWrapperView.alpha = 0.0f; _lockPanelWrapperView.alpha = 0.0f;
_lock.transform = CGAffineTransformIdentity;
if (iosMajorVersion() >= 8) { if (iosMajorVersion() >= 8) {
[UIView animateWithDuration:0.50 delay:0.0 usingSpringWithDamping:0.55f initialSpringVelocity:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^{ [UIView animateWithDuration:0.50 delay:0.0 usingSpringWithDamping:0.55f initialSpringVelocity:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^{
_innerCircleView.transform = CGAffineTransformIdentity; _innerCircleView.transform = CGAffineTransformIdentity;
@ -548,6 +550,10 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
} }
- (void)animateLock { - (void)animateLock {
if (!_animatedIn) {
return;
}
_lockView.lockness = 1.0f; _lockView.lockness = 1.0f;
[_lock updateLockness:1.0]; [_lock updateLockness:1.0];
@ -718,10 +724,6 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
return false; return false;
} else if (distanceX < -100.0 && !_xFeedbackOccured) { } else if (distanceX < -100.0 && !_xFeedbackOccured) {
if (iosMajorVersion() >= 10) {
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[generator impactOccurred];
}
_xFeedbackOccured = true; _xFeedbackOccured = true;
} else if (distanceX > -100.0) { } else if (distanceX > -100.0) {
_xFeedbackOccured = false; _xFeedbackOccured = false;
@ -732,10 +734,6 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
return false; return false;
} else if (distanceY < -60.0 && !_yFeedbackOccured) { } else if (distanceY < -60.0 && !_yFeedbackOccured) {
if (iosMajorVersion() >= 10) {
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[generator impactOccurred];
}
_yFeedbackOccured = true; _yFeedbackOccured = true;
} else if (distanceY > -60.0) { } else if (distanceY > -60.0) {
_yFeedbackOccured = false; _yFeedbackOccured = false;

View File

@ -628,7 +628,7 @@ typedef enum
_dismissed = cancelled; _dismissed = cancelled;
if (self.onDismiss != nil) if (self.onDismiss != nil)
self.onDismiss(_automaticDismiss); self.onDismiss(_automaticDismiss, cancelled);
if (_player != nil) if (_player != nil)
[_player pause]; [_player pause];

View File

@ -317,6 +317,8 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
} }
private func dismiss(forceAway: Bool) { private func dismiss(forceAway: Bool) {
self.animatedIn.set(false)
var animatedOutNode = true var animatedOutNode = true
var animatedOutInterface = false var animatedOutInterface = false

View File

@ -43,7 +43,7 @@ private enum StatsSection: Int32 {
} }
private enum StatsEntry: ItemListNodeEntry { private enum StatsEntry: ItemListNodeEntry {
case overviewHeader(PresentationTheme, String, String) case overviewTitle(PresentationTheme, String, String)
case overview(PresentationTheme, ChannelStats) case overview(PresentationTheme, ChannelStats)
case growthTitle(PresentationTheme, String) case growthTitle(PresentationTheme, String)
@ -78,7 +78,7 @@ private enum StatsEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .overviewHeader, .overview: case .overviewTitle, .overview:
return StatsSection.overview.rawValue return StatsSection.overview.rawValue
case .growthTitle, .growthGraph: case .growthTitle, .growthGraph:
return StatsSection.growth.rawValue return StatsSection.growth.rawValue
@ -105,7 +105,7 @@ private enum StatsEntry: ItemListNodeEntry {
var stableId: Int32 { var stableId: Int32 {
switch self { switch self {
case .overviewHeader: case .overviewTitle:
return 0 return 0
case .overview: case .overview:
return 1 return 1
@ -154,8 +154,8 @@ private enum StatsEntry: ItemListNodeEntry {
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool { static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
switch lhs { switch lhs {
case let .overviewHeader(lhsTheme, lhsText, lhsDates): case let .overviewTitle(lhsTheme, lhsText, lhsDates):
if case let .overviewHeader(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates { if case let .overviewTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true return true
} else { } else {
return false return false
@ -296,7 +296,7 @@ private enum StatsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelStatsControllerArguments let arguments = arguments as! ChannelStatsControllerArguments
switch self { switch self {
case let .overviewHeader(_, text, dates): case let .overviewTitle(_, text, dates):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
case let .growthTitle(_, text), case let .growthTitle(_, text),
let .followersTitle(_, text), let .followersTitle(_, text),
@ -343,7 +343,7 @@ private func channelStatsControllerEntries(data: ChannelStats?, messages: [Messa
let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings) let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings)
let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings) let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings)
entries.append(.overviewHeader(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) \(maxDate)")) entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) \(maxDate)"))
entries.append(.overview(presentationData.theme, data)) entries.append(.overview(presentationData.theme, data))
if !data.growthGraph.isEmpty { if !data.growthGraph.isEmpty {

View File

@ -21,11 +21,23 @@ private final class GroupStatsControllerArguments {
let context: AccountContext let context: AccountContext
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError> let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
let openPeer: (PeerId) -> Void let openPeer: (PeerId) -> Void
let openPeerHistory: (PeerId) -> Void
let openPeerAdminActions: (PeerId) -> Void
let promotePeer: (PeerId) -> Void
let setPostersPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let setAdminsPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let setInvitersPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPeer: @escaping (PeerId) -> Void) { init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPeer: @escaping (PeerId) -> Void, openPeerHistory: @escaping (PeerId) -> Void, openPeerAdminActions: @escaping (PeerId) -> Void, promotePeer: @escaping (PeerId) -> Void, setPostersPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setAdminsPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setInvitersPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) {
self.context = context self.context = context
self.loadDetailedGraph = loadDetailedGraph self.loadDetailedGraph = loadDetailedGraph
self.openPeer = openPeer self.openPeer = openPeer
self.openPeerHistory = openPeerHistory
self.openPeerAdminActions = openPeerAdminActions
self.promotePeer = promotePeer
self.setPostersPeerIdWithRevealedOptions = setPostersPeerIdWithRevealedOptions
self.setAdminsPeerIdWithRevealedOptions = setAdminsPeerIdWithRevealedOptions
self.setInvitersPeerIdWithRevealedOptions = setInvitersPeerIdWithRevealedOptions
} }
} }
@ -38,13 +50,14 @@ private enum StatsSection: Int32 {
case messages case messages
case actions case actions
case topHours case topHours
case topWeekdays
case topPosters case topPosters
case topAdmins case topAdmins
case topInviters case topInviters
} }
private enum StatsEntry: ItemListNodeEntry { private enum StatsEntry: ItemListNodeEntry {
case overviewHeader(PresentationTheme, String, String) case overviewTitle(PresentationTheme, String, String)
case overview(PresentationTheme, GroupStats) case overview(PresentationTheme, GroupStats)
case growthTitle(PresentationTheme, String) case growthTitle(PresentationTheme, String)
@ -68,18 +81,21 @@ private enum StatsEntry: ItemListNodeEntry {
case topHoursTitle(PresentationTheme, String) case topHoursTitle(PresentationTheme, String)
case topHoursGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case topHoursGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case topPostersTitle(PresentationTheme, String) case topWeekdaysTitle(PresentationTheme, String)
case topPoster(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopPoster) case topWeekdaysGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case topAdminsTitle(PresentationTheme, String) case topPostersTitle(PresentationTheme, String, String)
case topAdmin(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopAdmin) case topPoster(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopPoster, Bool)
case topInvitersTitle(PresentationTheme, String) case topAdminsTitle(PresentationTheme, String, String)
case topInviter(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopInviter) case topAdmin(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopAdmin, Bool)
case topInvitersTitle(PresentationTheme, String, String)
case topInviter(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopInviter, Bool)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .overviewHeader, .overview: case .overviewTitle, .overview:
return StatsSection.overview.rawValue return StatsSection.overview.rawValue
case .growthTitle, .growthGraph: case .growthTitle, .growthGraph:
return StatsSection.growth.rawValue return StatsSection.growth.rawValue
@ -95,6 +111,8 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.actions.rawValue return StatsSection.actions.rawValue
case .topHoursTitle, .topHoursGraph: case .topHoursTitle, .topHoursGraph:
return StatsSection.topHours.rawValue return StatsSection.topHours.rawValue
case .topWeekdaysTitle, .topWeekdaysGraph:
return StatsSection.topWeekdays.rawValue
case .topPostersTitle, .topPoster: case .topPostersTitle, .topPoster:
return StatsSection.topPosters.rawValue return StatsSection.topPosters.rawValue
case .topAdminsTitle, .topAdmin: case .topAdminsTitle, .topAdmin:
@ -106,7 +124,7 @@ private enum StatsEntry: ItemListNodeEntry {
var stableId: Int32 { var stableId: Int32 {
switch self { switch self {
case .overviewHeader: case .overviewTitle:
return 0 return 0
case .overview: case .overview:
return 1 return 1
@ -138,25 +156,29 @@ private enum StatsEntry: ItemListNodeEntry {
return 14 return 14
case .topHoursGraph: case .topHoursGraph:
return 15 return 15
case .topWeekdaysTitle:
return 16
case .topWeekdaysGraph:
return 17
case .topPostersTitle: case .topPostersTitle:
return 1000 return 1000
case let .topPoster(index, _, _, _, _, _): case let .topPoster(index, _, _, _, _, _, _):
return 1001 + index return 1001 + index
case .topAdminsTitle: case .topAdminsTitle:
return 2000 return 2000
case let .topAdmin(index, _, _, _, _, _): case let .topAdmin(index, _, _, _, _, _, _):
return 2001 + index return 2001 + index
case .topInvitersTitle: case .topInvitersTitle:
return 3000 return 3000
case let .topInviter(index, _, _, _, _, _): case let .topInviter(index, _, _, _, _, _, _):
return 30001 + index return 30001 + index
} }
} }
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool { static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
switch lhs { switch lhs {
case let .overviewHeader(lhsTheme, lhsText, lhsDates): case let .overviewTitle(lhsTheme, lhsText, lhsDates):
if case let .overviewHeader(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates { if case let .overviewTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true return true
} else { } else {
return false return false
@ -251,38 +273,50 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .topPostersTitle(lhsTheme, lhsText): case let .topWeekdaysTitle(lhsTheme, lhsText):
if case let .topPostersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .topWeekdaysTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .topWeekdaysGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType):
if case let .topWeekdaysGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType {
return true return true
} else { } else {
return false return false
} }
case let .topPoster(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopPoster): case let .topPostersTitle(lhsTheme, lhsText, lhsDates):
if case let .topPoster(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopPoster) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopPoster == rhsTopPoster { if case let .topPostersTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true return true
} else { } else {
return false return false
} }
case let .topAdminsTitle(lhsTheme, lhsText): case let .topPoster(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopPoster, lhsRevealed):
if case let .topAdminsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .topPoster(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopPoster, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopPoster == rhsTopPoster, lhsRevealed == rhsRevealed {
return true return true
} else { } else {
return false return false
} }
case let .topAdmin(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopAdmin): case let .topAdminsTitle(lhsTheme, lhsText, lhsDates):
if case let .topAdmin(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopAdmin) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopAdmin == rhsTopAdmin { if case let .topAdminsTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true return true
} else { } else {
return false return false
} }
case let .topInvitersTitle(lhsTheme, lhsText): case let .topAdmin(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopAdmin, lhsRevealed):
if case let .topInvitersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .topAdmin(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopAdmin, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopAdmin == rhsTopAdmin, lhsRevealed == rhsRevealed {
return true return true
} else { } else {
return false return false
} }
case let .topInviter(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopInviter): case let .topInvitersTitle(lhsTheme, lhsText, lhsDates):
if case let .topInviter(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopInviter) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopInviter == rhsTopInviter { if case let .topInvitersTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true
} else {
return false
}
case let .topInviter(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopInviter, lhsRevealed):
if case let .topInviter(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopInviter, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopInviter == rhsTopInviter, lhsRevealed == rhsRevealed {
return true return true
} else { } else {
return false return false
@ -297,7 +331,7 @@ private enum StatsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GroupStatsControllerArguments let arguments = arguments as! GroupStatsControllerArguments
switch self { switch self {
case let .overviewHeader(_, text, dates): case let .overviewTitle(_, text, dates):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
case let .growthTitle(_, text), case let .growthTitle(_, text),
let .membersTitle(_, text), let .membersTitle(_, text),
@ -306,10 +340,12 @@ private enum StatsEntry: ItemListNodeEntry {
let .messagesTitle(_, text), let .messagesTitle(_, text),
let .actionsTitle(_, text), let .actionsTitle(_, text),
let .topHoursTitle(_, text), let .topHoursTitle(_, text),
let .topPostersTitle(_, text), let .topWeekdaysTitle(_, text):
let .topAdminsTitle(_, text),
let .topInvitersTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .topPostersTitle(_, text, dates),
let .topAdminsTitle(_, text, dates),
let .topInvitersTitle(_, text, dates):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
case let .overview(_, stats): case let .overview(_, stats):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks) return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
case let .growthGraph(_, _, _, graph, type), case let .growthGraph(_, _, _, graph, type),
@ -318,9 +354,10 @@ private enum StatsEntry: ItemListNodeEntry {
let .languagesGraph(_, _, _, graph, type), let .languagesGraph(_, _, _, graph, type),
let .messagesGraph(_, _, _, graph, type), let .messagesGraph(_, _, _, graph, type),
let .actionsGraph(_, _, _, graph, type), let .actionsGraph(_, _, _, graph, type),
let .topHoursGraph(_, _, _, graph, type): let .topHoursGraph(_, _, _, graph, type),
let .topWeekdaysGraph(_, _, _, graph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks) return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
case let .topPoster(_, _, strings, dateTimeFormat, peer, topPoster): case let .topPoster(_, _, strings, dateTimeFormat, peer, topPoster, revealed):
var textComponents: [String] = [] var textComponents: [String] = []
if topPoster.messageCount > 0 { if topPoster.messageCount > 0 {
textComponents.append(strings.Stats_GroupTopPosterMessages(topPoster.messageCount)) textComponents.append(strings.Stats_GroupTopPosterMessages(topPoster.messageCount))
@ -328,10 +365,19 @@ private enum StatsEntry: ItemListNodeEntry {
textComponents.append(strings.Stats_GroupTopPosterChars(topPoster.averageChars)) textComponents.append(strings.Stats_GroupTopPosterChars(topPoster.averageChars))
} }
} }
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: { var options: [ItemListPeerItemRevealOption] = []
options.append(ItemListPeerItemRevealOption(type: .accent, title: strings.Stats_GroupTopPoster_History, action: {
arguments.openPeerHistory(peer.id)
}))
options.append(ItemListPeerItemRevealOption(type: .neutral, title: strings.Stats_GroupTopPoster_Promote, action: {
arguments.promotePeer(peer.id)
}))
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer.id) arguments.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
case let .topAdmin(_, _, strings, dateTimeFormat, peer, topAdmin): arguments.setPostersPeerIdWithRevealedOptions(peerId, fromPeerId)
}, removePeer: { _ in })
case let .topAdmin(_, _, strings, dateTimeFormat, peer, topAdmin, revealed):
var textComponents: [String] = [] var textComponents: [String] = []
if topAdmin.deletedCount > 0 { if topAdmin.deletedCount > 0 {
textComponents.append(strings.Stats_GroupTopAdminDeletions(topAdmin.deletedCount)) textComponents.append(strings.Stats_GroupTopAdminDeletions(topAdmin.deletedCount))
@ -342,29 +388,46 @@ private enum StatsEntry: ItemListNodeEntry {
if topAdmin.bannedCount > 0 { if topAdmin.bannedCount > 0 {
textComponents.append(strings.Stats_GroupTopAdminBans(topAdmin.bannedCount)) textComponents.append(strings.Stats_GroupTopAdminBans(topAdmin.bannedCount))
} }
var options: [ItemListPeerItemRevealOption] = []
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: { options.append(ItemListPeerItemRevealOption(type: .accent, title: strings.Stats_GroupTopAdmin_Actions, action: {
arguments.openPeerAdminActions(peer.id)
}))
options.append(ItemListPeerItemRevealOption(type: .neutral, title: strings.Stats_GroupTopAdmin_Promote, action: {
arguments.promotePeer(peer.id)
}))
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer.id) arguments.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
case let .topInviter(_, _, strings, dateTimeFormat, peer, topInviter): arguments.setAdminsPeerIdWithRevealedOptions(peerId, fromPeerId)
}, removePeer: { _ in })
case let .topInviter(_, _, strings, dateTimeFormat, peer, topInviter, revealed):
var textComponents: [String] = [] var textComponents: [String] = []
textComponents.append(strings.Stats_GroupTopInviterInvites(topInviter.inviteCount)) textComponents.append(strings.Stats_GroupTopInviterInvites(topInviter.inviteCount))
var options: [ItemListPeerItemRevealOption] = []
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: { options.append(ItemListPeerItemRevealOption(type: .accent, title: strings.Stats_GroupTopPoster_History, action: {
arguments.openPeerHistory(peer.id)
}))
options.append(ItemListPeerItemRevealOption(type: .neutral, title: strings.Stats_GroupTopPoster_Promote, action: {
arguments.promotePeer(peer.id)
}))
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(textComponents.joined(separator: ", ")), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer.id) arguments.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
arguments.setInvitersPeerIdWithRevealedOptions(peerId, fromPeerId)
}, removePeer: { _ in })
} }
} }
} }
private func groupStatsControllerEntries(data: GroupStats?, peers: [PeerId: Peer]?, presentationData: PresentationData) -> [StatsEntry] { private func groupStatsControllerEntries(state: GroupStatsState, data: GroupStats?, peers: [PeerId: Peer]?, presentationData: PresentationData) -> [StatsEntry] {
var entries: [StatsEntry] = [] var entries: [StatsEntry] = []
if let data = data { if let data = data {
let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings) let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings)
let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings) let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings)
let dates = "\(minDate) \(maxDate)"
entries.append(.overviewHeader(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) \(maxDate)")) entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_Overview, dates))
entries.append(.overview(presentationData.theme, data)) entries.append(.overview(presentationData.theme, data))
if !data.growthGraph.isEmpty { if !data.growthGraph.isEmpty {
@ -399,36 +462,41 @@ private func groupStatsControllerEntries(data: GroupStats?, peers: [PeerId: Peer
if !data.topHoursGraph.isEmpty { if !data.topHoursGraph.isEmpty {
entries.append(.topHoursTitle(presentationData.theme, presentationData.strings.Stats_GroupTopHoursTitle)) entries.append(.topHoursTitle(presentationData.theme, presentationData.strings.Stats_GroupTopHoursTitle))
entries.append(.topHoursGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .lines)) entries.append(.topHoursGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep))
}
if !data.topWeekdaysGraph.isEmpty {
entries.append(.topWeekdaysTitle(presentationData.theme, presentationData.strings.Stats_GroupTopWeekdaysTitle))
entries.append(.topWeekdaysGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topWeekdaysGraph, .area))
} }
if let peers = peers { if let peers = peers {
if !data.topPosters.isEmpty { if !data.topPosters.isEmpty {
entries.append(.topPostersTitle(presentationData.theme, presentationData.strings.Stats_GroupTopPostersTitle)) entries.append(.topPostersTitle(presentationData.theme, presentationData.strings.Stats_GroupTopPostersTitle, dates))
var index: Int32 = 0 var index: Int32 = 0
for topPoster in data.topPosters { for topPoster in data.topPosters {
if let peer = peers[topPoster.peerId], topPoster.messageCount > 0 { if let peer = peers[topPoster.peerId], topPoster.messageCount > 0 {
entries.append(.topPoster(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topPoster)) entries.append(.topPoster(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topPoster, topPoster.peerId == state.posterPeerIdWithRevealedOptions))
index += 1 index += 1
} }
} }
} }
if !data.topAdmins.isEmpty { if !data.topAdmins.isEmpty {
entries.append(.topAdminsTitle(presentationData.theme, presentationData.strings.Stats_GroupTopAdminsTitle)) entries.append(.topAdminsTitle(presentationData.theme, presentationData.strings.Stats_GroupTopAdminsTitle, dates))
var index: Int32 = 0 var index: Int32 = 0
for topAdmin in data.topAdmins { for topAdmin in data.topAdmins {
if let peer = peers[topAdmin.peerId], (topAdmin.deletedCount + topAdmin.kickedCount + topAdmin.bannedCount) > 0 { if let peer = peers[topAdmin.peerId], (topAdmin.deletedCount + topAdmin.kickedCount + topAdmin.bannedCount) > 0 {
entries.append(.topAdmin(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topAdmin)) entries.append(.topAdmin(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topAdmin, topAdmin.peerId == state.adminPeerIdWithRevealedOptions))
index += 1 index += 1
} }
} }
} }
if !data.topInviters.isEmpty { if !data.topInviters.isEmpty {
entries.append(.topInvitersTitle(presentationData.theme, presentationData.strings.Stats_GroupTopInvitersTitle)) entries.append(.topInvitersTitle(presentationData.theme, presentationData.strings.Stats_GroupTopInvitersTitle, dates))
var index: Int32 = 0 var index: Int32 = 0
for topInviter in data.topInviters { for topInviter in data.topInviters {
if let peer = peers[topInviter.peerId], topInviter.inviteCount > 0 { if let peer = peers[topInviter.peerId], topInviter.inviteCount > 0 {
entries.append(.topInviter(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topInviter)) entries.append(.topInviter(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, topInviter, topInviter.peerId == state.inviterPeerIdWithRevealedOptions))
index += 1 index += 1
} }
} }
@ -439,8 +507,55 @@ private func groupStatsControllerEntries(data: GroupStats?, peers: [PeerId: Peer
return entries return entries
} }
private struct GroupStatsState: Equatable {
let posterPeerIdWithRevealedOptions: PeerId?
let adminPeerIdWithRevealedOptions: PeerId?
let inviterPeerIdWithRevealedOptions: PeerId?
init() {
self.posterPeerIdWithRevealedOptions = nil
self.adminPeerIdWithRevealedOptions = nil
self.inviterPeerIdWithRevealedOptions = nil
}
init(posterPeerIdWithRevealedOptions: PeerId?, adminPeerIdWithRevealedOptions: PeerId?, inviterPeerIdWithRevealedOptions: PeerId?) {
self.posterPeerIdWithRevealedOptions = posterPeerIdWithRevealedOptions
self.adminPeerIdWithRevealedOptions = adminPeerIdWithRevealedOptions
self.inviterPeerIdWithRevealedOptions = inviterPeerIdWithRevealedOptions
}
static func ==(lhs: GroupStatsState, rhs: GroupStatsState) -> Bool {
if lhs.posterPeerIdWithRevealedOptions != rhs.posterPeerIdWithRevealedOptions {
return false
}
if lhs.adminPeerIdWithRevealedOptions != rhs.adminPeerIdWithRevealedOptions {
return false
}
if lhs.inviterPeerIdWithRevealedOptions != rhs.inviterPeerIdWithRevealedOptions {
return false
}
return true
}
func withUpdatedPosterPeerIdWithRevealedOptions(_ posterPeerIdWithRevealedOptions: PeerId?) -> GroupStatsState {
return GroupStatsState(posterPeerIdWithRevealedOptions: posterPeerIdWithRevealedOptions, adminPeerIdWithRevealedOptions: posterPeerIdWithRevealedOptions != nil ? nil : self.adminPeerIdWithRevealedOptions, inviterPeerIdWithRevealedOptions: posterPeerIdWithRevealedOptions != nil ? nil : self.inviterPeerIdWithRevealedOptions)
}
func withUpdatedAdminPeerIdWithRevealedOptions(_ adminPeerIdWithRevealedOptions: PeerId?) -> GroupStatsState {
return GroupStatsState(posterPeerIdWithRevealedOptions: adminPeerIdWithRevealedOptions != nil ? nil : self.posterPeerIdWithRevealedOptions, adminPeerIdWithRevealedOptions: adminPeerIdWithRevealedOptions, inviterPeerIdWithRevealedOptions: adminPeerIdWithRevealedOptions != nil ? nil : self.inviterPeerIdWithRevealedOptions)
}
func withUpdatedInviterPeerIdWithRevealedOptions(_ inviterPeerIdWithRevealedOptions: PeerId?) -> GroupStatsState {
return GroupStatsState(posterPeerIdWithRevealedOptions: inviterPeerIdWithRevealedOptions != nil ? nil : self.posterPeerIdWithRevealedOptions, adminPeerIdWithRevealedOptions: inviterPeerIdWithRevealedOptions != nil ? nil : self.adminPeerIdWithRevealedOptions, inviterPeerIdWithRevealedOptions: inviterPeerIdWithRevealedOptions)
}
}
public func groupStatsController(context: AccountContext, peerId: PeerId, cachedPeerData: CachedPeerData) -> ViewController { public func groupStatsController(context: AccountContext, peerId: PeerId, cachedPeerData: CachedPeerData) -> ViewController {
var openPeerImpl: ((PeerId) -> Void)? let statePromise = ValuePromise(GroupStatsState())
let stateValue = Atomic(value: GroupStatsState())
let updateState: ((GroupStatsState) -> GroupStatsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let dataPromise = Promise<GroupStats?>(nil) let dataPromise = Promise<GroupStats?>(nil)
@ -450,6 +565,14 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
if let cachedData = cachedPeerData as? CachedChannelData { if let cachedData = cachedPeerData as? CachedChannelData {
datacenterId = cachedData.statsDatacenterId datacenterId = cachedData.statsDatacenterId
} }
var openPeerImpl: ((PeerId) -> Void)?
var openPeerHistoryImpl: ((PeerId) -> Void)?
var openPeerAdminActionsImpl: ((PeerId) -> Void)?
var promotePeerImpl: ((PeerId) -> Void)?
let peerView = Promise<PeerView>()
peerView.set(context.account.viewTracker.peerView(peerId, updateData: true))
let statsContext = GroupStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peerId) let statsContext = GroupStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peerId)
let dataSignal: Signal<GroupStats?, NoError> = statsContext.state let dataSignal: Signal<GroupStats?, NoError> = statsContext.state
@ -457,7 +580,7 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
return state.stats return state.stats
} |> afterNext({ [weak statsContext] stats in } |> afterNext({ [weak statsContext] stats in
if let statsContext = statsContext, let stats = stats { if let statsContext = statsContext, let stats = stats {
if case .OnDemand = stats.newMembersBySourceGraph { if case .OnDemand = stats.topWeekdaysGraph {
statsContext.loadGrowthGraph() statsContext.loadGrowthGraph()
statsContext.loadMembersGraph() statsContext.loadMembersGraph()
statsContext.loadNewMembersBySourceGraph() statsContext.loadNewMembersBySourceGraph()
@ -465,6 +588,7 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
statsContext.loadMessagesGraph() statsContext.loadMessagesGraph()
statsContext.loadActionsGraph() statsContext.loadActionsGraph()
statsContext.loadTopHoursGraph() statsContext.loadTopHoursGraph()
statsContext.loadTopWeekdaysGraph()
} }
} }
}) })
@ -503,15 +627,45 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
return statsContext.loadDetailedGraph(graph, x: x) return statsContext.loadDetailedGraph(graph, x: x)
}, openPeer: { peerId in }, openPeer: { peerId in
openPeerImpl?(peerId) openPeerImpl?(peerId)
}, openPeerHistory: { peerId in
openPeerHistoryImpl?(peerId)
}, openPeerAdminActions: { peerId in
openPeerAdminActionsImpl?(peerId)
}, promotePeer: { peerId in
promotePeerImpl?(peerId)
}, setPostersPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.posterPeerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPosterPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, setAdminsPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.adminPeerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedAdminPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, setInvitersPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.inviterPeerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedInviterPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}) })
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue()))
let previousData = Atomic<GroupStats?>(value: nil) let previousData = Atomic<GroupStats?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), peersPromise.get(), longLoadingSignal) let signal = combineLatest(statePromise.get(), context.sharedContext.presentationData, dataPromise.get(), peersPromise.get(), longLoadingSignal)
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { presentationData, data, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { state, presentationData, data, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data) let previous = previousData.swap(data)
var emptyStateItem: ItemListControllerEmptyStateItem? var emptyStateItem: ItemListControllerEmptyStateItem?
if data == nil { if data == nil {
@ -523,7 +677,7 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStatsControllerEntries(data: data, peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStatsControllerEntries(state: state, data: data, peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -554,5 +708,35 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
}) })
} }
} }
openPeerHistoryImpl = { [weak controller] participantPeerId in
if let navigationController = controller?.navigationController as? NavigationController {
let _ = (context.account.postbox.loadedPeerWithId(participantPeerId)
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peerId), subject: nil, botStart: nil, updateTextInputState: nil, activateInput: false, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: (.member(peer), ""), animated: true))
})
}
}
openPeerAdminActionsImpl = { [weak controller] participantPeerId in
if let navigationController = controller?.navigationController as? NavigationController {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer)
navigationController.pushViewController(controller)
})
}
}
promotePeerImpl = { [weak controller] participantPeerId in
if let navigationController = controller?.navigationController as? NavigationController {
let _ = (fetchChannelParticipant(account: context.account, peerId: peerId, participantId: participantPeerId)
|> take(1)
|> deliverOnMainQueue).start(next: { participant in
if let participant = participant, let controller = context.sharedContext.makeChannelAdminController(context: context, peerId: peerId, adminId: participantPeerId, initialParticipant: participant) {
navigationController.pushViewController(controller)
}
})
}
}
return controller return controller
} }

View File

@ -544,7 +544,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1918567619] = { return Api.Updates.parse_updatesCombined($0) } dict[1918567619] = { return Api.Updates.parse_updatesCombined($0) }
dict[1957577280] = { return Api.Updates.parse_updates($0) } dict[1957577280] = { return Api.Updates.parse_updates($0) }
dict[301019932] = { return Api.Updates.parse_updateShortSentMessage($0) } dict[301019932] = { return Api.Updates.parse_updateShortSentMessage($0) }
dict[447818040] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) } dict[-276825834] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) }
dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) } dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) }
dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) } dict[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) }
dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) } dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) }

View File

@ -663,13 +663,13 @@ public struct stats {
} }
public enum MegagroupStats: TypeConstructorDescription { public enum MegagroupStats: TypeConstructorDescription {
case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User]) case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let topPosters, let topAdmins, let topInviters, let users): case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users):
if boxed { if boxed {
buffer.appendInt32(447818040) buffer.appendInt32(-276825834)
} }
period.serialize(buffer, true) period.serialize(buffer, true)
members.serialize(buffer, true) members.serialize(buffer, true)
@ -683,6 +683,7 @@ public struct stats {
messagesGraph.serialize(buffer, true) messagesGraph.serialize(buffer, true)
actionsGraph.serialize(buffer, true) actionsGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true) topHoursGraph.serialize(buffer, true)
weekdaysGraph.serialize(buffer, true)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topPosters.count)) buffer.appendInt32(Int32(topPosters.count))
for item in topPosters { for item in topPosters {
@ -709,8 +710,8 @@ public struct stats {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let topPosters, let topAdmins, let topInviters, let users): case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users):
return ("megagroupStats", [("period", period), ("members", members), ("messages", messages), ("viewers", viewers), ("posters", posters), ("growthGraph", growthGraph), ("membersGraph", membersGraph), ("newMembersBySourceGraph", newMembersBySourceGraph), ("languagesGraph", languagesGraph), ("messagesGraph", messagesGraph), ("actionsGraph", actionsGraph), ("topHoursGraph", topHoursGraph), ("topPosters", topPosters), ("topAdmins", topAdmins), ("topInviters", topInviters), ("users", users)]) return ("megagroupStats", [("period", period), ("members", members), ("messages", messages), ("viewers", viewers), ("posters", posters), ("growthGraph", growthGraph), ("membersGraph", membersGraph), ("newMembersBySourceGraph", newMembersBySourceGraph), ("languagesGraph", languagesGraph), ("messagesGraph", messagesGraph), ("actionsGraph", actionsGraph), ("topHoursGraph", topHoursGraph), ("weekdaysGraph", weekdaysGraph), ("topPosters", topPosters), ("topAdmins", topAdmins), ("topInviters", topInviters), ("users", users)])
} }
} }
@ -763,21 +764,25 @@ public struct stats {
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph _12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
} }
var _13: [Api.StatsGroupTopPoster]? var _13: Api.StatsGraph?
if let _ = reader.readInt32() { if let signature = reader.readInt32() {
_13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self) _13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
} }
var _14: [Api.StatsGroupTopAdmin]? var _14: [Api.StatsGroupTopPoster]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopAdmin.self) _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self)
} }
var _15: [Api.StatsGroupTopInviter]? var _15: [Api.StatsGroupTopAdmin]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopInviter.self) _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopAdmin.self)
} }
var _16: [Api.User]? var _16: [Api.StatsGroupTopInviter]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopInviter.self)
}
var _17: [Api.User]?
if let _ = reader.readInt32() {
_17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
} }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
@ -795,8 +800,9 @@ public struct stats {
let _c14 = _14 != nil let _c14 = _14 != nil
let _c15 = _15 != nil let _c15 = _15 != nil
let _c16 = _16 != nil let _c16 = _16 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { let _c17 = _17 != nil
return Api.stats.MegagroupStats.megagroupStats(period: _1!, members: _2!, messages: _3!, viewers: _4!, posters: _5!, growthGraph: _6!, membersGraph: _7!, newMembersBySourceGraph: _8!, languagesGraph: _9!, messagesGraph: _10!, actionsGraph: _11!, topHoursGraph: _12!, topPosters: _13!, topAdmins: _14!, topInviters: _15!, users: _16!) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
return Api.stats.MegagroupStats.megagroupStats(period: _1!, members: _2!, messages: _3!, viewers: _4!, posters: _5!, growthGraph: _6!, membersGraph: _7!, newMembersBySourceGraph: _8!, languagesGraph: _9!, messagesGraph: _10!, actionsGraph: _11!, topHoursGraph: _12!, weekdaysGraph: _13!, topPosters: _14!, topAdmins: _15!, topInviters: _16!, users: _17!)
} }
else { else {
return nil return nil

View File

@ -178,6 +178,10 @@ public final class CallController: ViewController {
let _ = self?.call.hangUp() let _ = self?.call.hangUp()
} }
self.controllerNode.toggleVideo = { [weak self] in
let _ = self?.call.setEnableVideo(true)
}
self.controllerNode.back = { [weak self] in self.controllerNode.back = { [weak self] in
let _ = self?.dismiss() let _ = self?.dismiss()
} }

View File

@ -11,6 +11,7 @@ enum CallControllerButtonType {
case accept case accept
case speaker case speaker
case bluetooth case bluetooth
case video
} }
private let buttonSize = CGSize(width: 75.0, height: 75.0) private let buttonSize = CGSize(width: 75.0, height: 75.0)
@ -123,6 +124,11 @@ final class CallControllerButtonNode: HighlightTrackingButtonNode {
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear) regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
case .video:
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: .white)
regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true)
} }
self.regularImage = regularImage self.regularImage = regularImage
@ -209,6 +215,11 @@ final class CallControllerButtonNode: HighlightTrackingButtonNode {
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear) regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true)
case .video:
let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: .white)
regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true)
} }
self.regularImage = regularImage self.regularImage = regularImage

View File

@ -15,7 +15,13 @@ enum CallControllerButtonsSpeakerMode {
} }
enum CallControllerButtonsMode: Equatable { enum CallControllerButtonsMode: Equatable {
case active(CallControllerButtonsSpeakerMode) enum VideoState: Equatable {
case notAvailable
case available(Bool)
case active
}
case active(speakerMode: CallControllerButtonsSpeakerMode, videoState: VideoState)
case incoming case incoming
} }
@ -27,6 +33,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
private let endButton: CallControllerButtonNode private let endButton: CallControllerButtonNode
private let speakerButton: CallControllerButtonNode private let speakerButton: CallControllerButtonNode
private let videoButton: CallControllerButtonNode
private var mode: CallControllerButtonsMode? private var mode: CallControllerButtonsMode?
private var validLayout: CGFloat? private var validLayout: CGFloat?
@ -41,6 +49,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
var mute: (() -> Void)? var mute: (() -> Void)?
var end: (() -> Void)? var end: (() -> Void)?
var speaker: (() -> Void)? var speaker: (() -> Void)?
var toggleVideo: (() -> Void)?
init(strings: PresentationStrings) { init(strings: PresentationStrings) {
self.acceptButton = CallControllerButtonNode(type: .accept, label: strings.Call_Accept) self.acceptButton = CallControllerButtonNode(type: .accept, label: strings.Call_Accept)
@ -55,6 +64,9 @@ final class CallControllerButtonsNode: ASDisplayNode {
self.speakerButton = CallControllerButtonNode(type: .speaker, label: nil) self.speakerButton = CallControllerButtonNode(type: .speaker, label: nil)
self.speakerButton.alpha = 0.0 self.speakerButton.alpha = 0.0
self.videoButton = CallControllerButtonNode(type: .video, label: nil)
self.videoButton.alpha = 0.0
super.init() super.init()
self.addSubnode(self.acceptButton) self.addSubnode(self.acceptButton)
@ -62,12 +74,14 @@ final class CallControllerButtonsNode: ASDisplayNode {
self.addSubnode(self.muteButton) self.addSubnode(self.muteButton)
self.addSubnode(self.endButton) self.addSubnode(self.endButton)
self.addSubnode(self.speakerButton) self.addSubnode(self.speakerButton)
self.addSubnode(self.videoButton)
self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
self.videoButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
} }
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) { func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
@ -107,6 +121,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0) var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0)
for button in [self.muteButton, self.endButton, self.speakerButton] { for button in [self.muteButton, self.endButton, self.speakerButton] {
transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize)) transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize))
if button === self.endButton {
transition.updateFrame(node: self.videoButton, frame: CGRect(origin: CGPoint(x: origin.x, y: origin.y - buttonSize.height - 20.0), size: buttonSize))
}
origin.x += buttonSize.width + threeButtonSpacing origin.x += buttonSize.width + threeButtonSpacing
} }
@ -121,10 +140,10 @@ final class CallControllerButtonsNode: ASDisplayNode {
for button in [self.declineButton, self.acceptButton] { for button in [self.declineButton, self.acceptButton] {
button.alpha = 1.0 button.alpha = 1.0
} }
for button in [self.muteButton, self.endButton, self.speakerButton] { for button in [self.muteButton, self.endButton, self.speakerButton, self.videoButton] {
button.alpha = 0.0 button.alpha = 0.0
} }
case let .active(speakerMode): case let .active(speakerMode, videoState):
for button in [self.muteButton, self.speakerButton] { for button in [self.muteButton, self.speakerButton] {
if animated && button.alpha.isZero { if animated && button.alpha.isZero {
button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
@ -152,6 +171,23 @@ final class CallControllerButtonsNode: ASDisplayNode {
self.endButton.alpha = 1.0 self.endButton.alpha = 1.0
} }
switch videoState {
case .notAvailable:
self.videoButton.alpha = 0.0
case let .available(isEnabled):
self.videoButton.isUserInteractionEnabled = isEnabled
if animated {
self.videoButton.alpha = isEnabled ? 1.0 : 0.5
self.videoButton.layer.animateAlpha(from: 0.0, to: self.videoButton.alpha, duration: 0.2)
} else {
self.videoButton.alpha = isEnabled ? 1.0 : 0.5
}
case .active:
self.videoButton.isUserInteractionEnabled = true
self.videoButton.alpha = 0.0
}
if !self.declineButton.alpha.isZero { if !self.declineButton.alpha.isZero {
if animated { if animated {
self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
@ -187,6 +223,26 @@ final class CallControllerButtonsNode: ASDisplayNode {
self.speaker?() self.speaker?()
} else if button === self.acceptButton { } else if button === self.acceptButton {
self.accept?() self.accept?()
} else if button === self.videoButton {
self.toggleVideo?()
} }
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let buttons = [
self.acceptButton,
self.declineButton,
self.muteButton,
self.endButton,
self.speakerButton,
self.videoButton
]
for button in buttons {
if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
} }

View File

@ -14,6 +14,49 @@ import LocalizedPeerData
import PhotoResources import PhotoResources
import CallsEmoji import CallsEmoji
private final class IncomingVideoNode: ASDisplayNode {
private let videoView: UIView
init(videoView: UIView) {
self.videoView = videoView
super.init()
self.view.addSubview(self.videoView)
}
func updateLayout(size: CGSize) {
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
}
}
private final class OutgoingVideoNode: ASDisplayNode {
private let videoView: UIView
private let switchCameraButton: HighlightableButtonNode
private let switchCamera: () -> Void
init(videoView: UIView, switchCamera: @escaping () -> Void) {
self.videoView = videoView
self.switchCameraButton = HighlightableButtonNode()
self.switchCamera = switchCamera
super.init()
self.view.addSubview(self.videoView)
self.addSubnode(self.switchCameraButton)
self.switchCameraButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.switchCamera()
}
func updateLayout(size: CGSize) {
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
self.switchCameraButton.frame = CGRect(origin: CGPoint(), size: size)
}
}
final class CallControllerNode: ASDisplayNode { final class CallControllerNode: ASDisplayNode {
private let sharedContext: SharedAccountContext private let sharedContext: SharedAccountContext
private let account: Account private let account: Account
@ -31,8 +74,8 @@ final class CallControllerNode: ASDisplayNode {
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
private var incomingVideoView: UIView? private var incomingVideoNode: IncomingVideoNode?
private var outgoingVideoView: UIView? private var outgoingVideoNode: OutgoingVideoNode?
private var videoViewsRequested: Bool = false private var videoViewsRequested: Bool = false
private let backButtonArrowNode: ASImageNode private let backButtonArrowNode: ASImageNode
private let backButtonNode: HighlightableButtonNode private let backButtonNode: HighlightableButtonNode
@ -63,6 +106,7 @@ final class CallControllerNode: ASDisplayNode {
var beginAudioOuputSelection: (() -> Void)? var beginAudioOuputSelection: (() -> Void)?
var acceptCall: (() -> Void)? var acceptCall: (() -> Void)?
var endCall: (() -> Void)? var endCall: (() -> Void)?
var toggleVideo: (() -> Void)?
var back: (() -> Void)? var back: (() -> Void)?
var presentCallRating: ((CallId) -> Void)? var presentCallRating: ((CallId) -> Void)?
var callEnded: ((Bool) -> Void)? var callEnded: ((Bool) -> Void)?
@ -151,6 +195,10 @@ final class CallControllerNode: ASDisplayNode {
self?.acceptCall?() self?.acceptCall?()
} }
self.buttonsNode.toggleVideo = { [weak self] in
self?.toggleVideo?()
}
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside) self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
@ -205,7 +253,58 @@ final class CallControllerNode: ASDisplayNode {
let statusValue: CallControllerStatusValue let statusValue: CallControllerStatusValue
var statusReception: Int32? var statusReception: Int32?
switch callState {
switch callState.videoState {
case .active:
if !self.videoViewsRequested {
self.videoViewsRequested = true
self.call.makeIncomingVideoView(completion: { [weak self] incomingVideoView in
guard let strongSelf = self else {
return
}
if let incomingVideoView = incomingVideoView {
strongSelf.setCurrentAudioOutput?(.speaker)
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
strongSelf.incomingVideoNode = incomingVideoNode
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
})
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
guard let strongSelf = self else {
return
}
if let outgoingVideoView = outgoingVideoView {
outgoingVideoView.backgroundColor = .black
outgoingVideoView.clipsToBounds = true
outgoingVideoView.layer.cornerRadius = 16.0
strongSelf.setCurrentAudioOutput?(.speaker)
let outgoingVideoNode = OutgoingVideoNode(videoView: outgoingVideoView, switchCamera: {
guard let strongSelf = self else {
return
}
strongSelf.call.switchVideoCamera()
})
strongSelf.outgoingVideoNode = outgoingVideoNode
if let incomingVideoNode = strongSelf.incomingVideoNode {
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: incomingVideoNode)
} else {
strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode)
}
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
})
}
default:
break
}
switch callState.state {
case .waiting, .connecting: case .waiting, .connecting:
statusValue = .text(self.presentationData.strings.Call_StatusConnecting) statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
case let .requesting(ringing): case let .requesting(ringing):
@ -241,7 +340,7 @@ final class CallControllerNode: ASDisplayNode {
case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash): case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash):
let strings = self.presentationData.strings let strings = self.presentationData.strings
var isReconnecting = false var isReconnecting = false
if case .reconnecting = callState { if case .reconnecting = callState.state {
isReconnecting = true isReconnecting = true
} }
statusValue = .timer({ value in statusValue = .timer({ value in
@ -266,45 +365,8 @@ final class CallControllerNode: ASDisplayNode {
} }
} }
statusReception = reception statusReception = reception
if !self.videoViewsRequested {
self.videoViewsRequested = true
self.call.makeIncomingVideoView(completion: { [weak self] incomingVideoView in
guard let strongSelf = self else {
return
}
if let incomingVideoView = incomingVideoView {
strongSelf.setCurrentAudioOutput?(.speaker)
strongSelf.incomingVideoView = incomingVideoView
strongSelf.containerNode.view.insertSubview(incomingVideoView, aboveSubview: strongSelf.dimNode.view)
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
})
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
guard let strongSelf = self else {
return
}
if let outgoingVideoView = outgoingVideoView {
outgoingVideoView.backgroundColor = .black
outgoingVideoView.clipsToBounds = true
outgoingVideoView.layer.cornerRadius = 16.0
strongSelf.setCurrentAudioOutput?(.speaker)
strongSelf.outgoingVideoView = outgoingVideoView
if let incomingVideoView = strongSelf.incomingVideoView {
strongSelf.containerNode.view.insertSubview(outgoingVideoView, aboveSubview: incomingVideoView)
} else {
strongSelf.containerNode.view.insertSubview(outgoingVideoView, aboveSubview: strongSelf.dimNode.view)
}
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
})
}
} }
switch callState { switch callState.state {
case .terminated, .terminating: case .terminated, .terminating:
if !self.statusNode.alpha.isEqual(to: 0.5) { if !self.statusNode.alpha.isEqual(to: 0.5) {
self.statusNode.alpha = 0.5 self.statusNode.alpha = 0.5
@ -327,7 +389,7 @@ final class CallControllerNode: ASDisplayNode {
} }
} }
if self.shouldStayHiddenUntilConnection { if self.shouldStayHiddenUntilConnection {
switch callState { switch callState.state {
case .connecting, .active: case .connecting, .active:
self.containerNode.alpha = 1.0 self.containerNode.alpha = 1.0
default: default:
@ -339,7 +401,7 @@ final class CallControllerNode: ASDisplayNode {
self.updateButtonsMode() self.updateButtonsMode()
if case let .terminated(id, _, reportRating) = callState, let callId = id { if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
let presentRating = reportRating || self.forceReportRating let presentRating = reportRating || self.forceReportRating
if presentRating { if presentRating {
self.presentCallRating?(callId) self.presentCallRating?(callId)
@ -353,7 +415,7 @@ final class CallControllerNode: ASDisplayNode {
return return
} }
switch callState { switch callState.state {
case .ringing: case .ringing:
self.buttonsNode.updateMode(.incoming) self.buttonsNode.updateMode(.incoming)
default: default:
@ -373,7 +435,16 @@ final class CallControllerNode: ASDisplayNode {
mode = .none mode = .none
} }
} }
self.buttonsNode.updateMode(.active(mode)) let mappedVideoState: CallControllerButtonsMode.VideoState
switch callState.videoState {
case .notAvailable:
mappedVideoState = .notAvailable
case .available:
mappedVideoState = .available(true)
case .active:
mappedVideoState = .active
}
self.buttonsNode.updateMode(.active(speakerMode: mode, videoState: mappedVideoState))
} }
} }
@ -466,12 +537,15 @@ final class CallControllerNode: ASDisplayNode {
let buttonsOriginY: CGFloat = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom let buttonsOriginY: CGFloat = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight))) transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
if let incomingVideoView = self.incomingVideoView { if let incomingVideoNode = self.incomingVideoNode {
incomingVideoView.frame = CGRect(origin: CGPoint(), size: layout.size) incomingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size)
incomingVideoNode.updateLayout(size: layout.size)
} }
if let outgoingVideoView = self.outgoingVideoView { if let outgoingVideoNode = self.outgoingVideoNode {
let outgoingSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0)) let outgoingSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
outgoingVideoView.frame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - outgoingSize.width, y: buttonsOriginY - 32.0 - outgoingSize.height), size: outgoingSize) let outgoingFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - outgoingSize.width, y: buttonsOriginY - 32.0 - outgoingSize.height), size: outgoingSize)
outgoingVideoNode.frame = outgoingFrame
outgoingVideoNode.updateLayout(size: outgoingFrame.size)
} }
let keyTextSize = self.keyButtonNode.frame.size let keyTextSize = self.keyButtonNode.frame.size

View File

@ -188,7 +188,7 @@ public final class PresentationCallImpl: PresentationCall {
private var sessionStateDisposable: Disposable? private var sessionStateDisposable: Disposable?
private let statePromise = ValuePromise<PresentationCallState>(.waiting, ignoreRepeated: true) private let statePromise = ValuePromise<PresentationCallState>(PresentationCallState(state: .waiting, videoState: .notAvailable), ignoreRepeated: true)
public var state: Signal<PresentationCallState, NoError> { public var state: Signal<PresentationCallState, NoError> {
return self.statePromise.get() return self.statePromise.get()
} }
@ -402,7 +402,7 @@ public final class PresentationCallImpl: PresentationCall {
switch sessionState.state { switch sessionState.state {
case .ringing: case .ringing:
presentationState = .ringing presentationState = PresentationCallState(state: .ringing, videoState: .notAvailable)
if previous == nil || previousControl == nil { if previous == nil || previousControl == nil {
if !self.reportedIncomingCall { if !self.reportedIncomingCall {
self.reportedIncomingCall = true self.reportedIncomingCall = true
@ -429,19 +429,28 @@ public final class PresentationCallImpl: PresentationCall {
} }
case .accepting: case .accepting:
self.callWasActive = true self.callWasActive = true
presentationState = .connecting(nil) presentationState = PresentationCallState(state: .connecting(nil), videoState: .notAvailable)
case .dropping: case .dropping:
presentationState = .terminating presentationState = PresentationCallState(state: .terminating, videoState: .notAvailable)
case let .terminated(id, reason, options): case let .terminated(id, reason, options):
presentationState = .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)) presentationState = PresentationCallState(state: .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)), videoState: .notAvailable)
case let .requesting(ringing): case let .requesting(ringing):
presentationState = .requesting(ringing) presentationState = PresentationCallState(state: .requesting(ringing), videoState: .notAvailable)
case let .active(_, _, keyVisualHash, _, _, _, _): case let .active(_, _, keyVisualHash, _, _, _, _):
self.callWasActive = true self.callWasActive = true
if let callContextState = callContextState { if let callContextState = callContextState {
switch callContextState { let mappedVideoState: PresentationCallState.VideoState
switch callContextState.videoState {
case .notAvailable:
mappedVideoState = .notAvailable
case let .available(enabled):
mappedVideoState = .available(enabled)
case .active:
mappedVideoState = .active
}
switch callContextState.state {
case .initializing: case .initializing:
presentationState = .connecting(keyVisualHash) presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState)
case .failed: case .failed:
presentationState = nil presentationState = nil
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
@ -453,7 +462,7 @@ public final class PresentationCallImpl: PresentationCall {
timestamp = CFAbsoluteTimeGetCurrent() timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp self.activeTimestamp = timestamp
} }
presentationState = .active(timestamp, reception, keyVisualHash) presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState)
case .reconnecting: case .reconnecting:
let timestamp: Double let timestamp: Double
if let activeTimestamp = self.activeTimestamp { if let activeTimestamp = self.activeTimestamp {
@ -462,10 +471,10 @@ public final class PresentationCallImpl: PresentationCall {
timestamp = CFAbsoluteTimeGetCurrent() timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp self.activeTimestamp = timestamp
} }
presentationState = .reconnecting(timestamp, reception, keyVisualHash) presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState)
} }
} else { } else {
presentationState = .connecting(keyVisualHash) presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: .notAvailable)
} }
} }
@ -555,12 +564,12 @@ public final class PresentationCallImpl: PresentationCall {
private func updateTone(_ state: PresentationCallState, callContextState: OngoingCallContextState?, previous: CallSession?) { private func updateTone(_ state: PresentationCallState, callContextState: OngoingCallContextState?, previous: CallSession?) {
var tone: PresentationCallTone? var tone: PresentationCallTone?
if let callContextState = callContextState, case .reconnecting = callContextState { if let callContextState = callContextState, case .reconnecting = callContextState.state {
tone = .connecting tone = .connecting
} else if let previous = previous { } else if let previous = previous {
switch previous.state { switch previous.state {
case .accepting, .active, .dropping, .requesting: case .accepting, .active, .dropping, .requesting:
switch state { switch state.state {
case .connecting: case .connecting:
if case .requesting = previous.state { if case .requesting = previous.state {
tone = .ringing tone = .ringing
@ -652,6 +661,14 @@ public final class PresentationCallImpl: PresentationCall {
self.ongoingContext?.setIsMuted(self.isMutedValue) self.ongoingContext?.setIsMuted(self.isMutedValue)
} }
public func setEnableVideo(_ value: Bool) {
self.ongoingContext?.setEnableVideo(value)
}
public func switchVideoCamera() {
self.ongoingContext?.switchVideoCamera()
}
public func setCurrentAudioOutput(_ output: AudioSessionOutput) { public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
guard self.currentAudioOutputValue != output else { guard self.currentAudioOutputValue != output else {
return return

View File

@ -553,11 +553,12 @@ public final class GroupStats: Equatable {
public let messagesGraph: StatsGraph public let messagesGraph: StatsGraph
public let actionsGraph: StatsGraph public let actionsGraph: StatsGraph
public let topHoursGraph: StatsGraph public let topHoursGraph: StatsGraph
public let topWeekdaysGraph: StatsGraph
public let topPosters: [GroupStatsTopPoster] public let topPosters: [GroupStatsTopPoster]
public let topAdmins: [GroupStatsTopAdmin] public let topAdmins: [GroupStatsTopAdmin]
public let topInviters: [GroupStatsTopInviter] public let topInviters: [GroupStatsTopInviter]
init(period: StatsDateRange, members: StatsValue, messages: StatsValue, viewers: StatsValue, posters: StatsValue, growthGraph: StatsGraph, membersGraph: StatsGraph, newMembersBySourceGraph: StatsGraph, languagesGraph: StatsGraph, messagesGraph: StatsGraph, actionsGraph: StatsGraph, topHoursGraph: StatsGraph, topPosters: [GroupStatsTopPoster], topAdmins: [GroupStatsTopAdmin], topInviters: [GroupStatsTopInviter]) { init(period: StatsDateRange, members: StatsValue, messages: StatsValue, viewers: StatsValue, posters: StatsValue, growthGraph: StatsGraph, membersGraph: StatsGraph, newMembersBySourceGraph: StatsGraph, languagesGraph: StatsGraph, messagesGraph: StatsGraph, actionsGraph: StatsGraph, topHoursGraph: StatsGraph, topWeekdaysGraph: StatsGraph, topPosters: [GroupStatsTopPoster], topAdmins: [GroupStatsTopAdmin], topInviters: [GroupStatsTopInviter]) {
self.period = period self.period = period
self.members = members self.members = members
self.messages = messages self.messages = messages
@ -570,6 +571,7 @@ public final class GroupStats: Equatable {
self.messagesGraph = messagesGraph self.messagesGraph = messagesGraph
self.actionsGraph = actionsGraph self.actionsGraph = actionsGraph
self.topHoursGraph = topHoursGraph self.topHoursGraph = topHoursGraph
self.topWeekdaysGraph = topWeekdaysGraph
self.topPosters = topPosters self.topPosters = topPosters
self.topAdmins = topAdmins self.topAdmins = topAdmins
self.topInviters = topInviters self.topInviters = topInviters
@ -612,6 +614,9 @@ public final class GroupStats: Equatable {
if lhs.topHoursGraph != rhs.topHoursGraph { if lhs.topHoursGraph != rhs.topHoursGraph {
return false return false
} }
if lhs.topWeekdaysGraph != rhs.topWeekdaysGraph {
return false
}
if lhs.topPosters != rhs.topPosters { if lhs.topPosters != rhs.topPosters {
return false return false
} }
@ -625,31 +630,35 @@ public final class GroupStats: Equatable {
} }
public func withUpdatedGrowthGraph(_ growthGraph: StatsGraph) -> GroupStats { public func withUpdatedGrowthGraph(_ growthGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
public func withUpdatedMembersGraph(_ membersGraph: StatsGraph) -> GroupStats { public func withUpdatedMembersGraph(_ membersGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
public func withUpdatedNewMembersBySourceGraph(_ newMembersBySourceGraph: StatsGraph) -> GroupStats { public func withUpdatedNewMembersBySourceGraph(_ newMembersBySourceGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
public func withUpdatedLanguagesGraph(_ languagesGraph: StatsGraph) -> GroupStats { public func withUpdatedLanguagesGraph(_ languagesGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
public func withUpdatedMessagesGraph(_ messagesGraph: StatsGraph) -> GroupStats { public func withUpdatedMessagesGraph(_ messagesGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
public func withUpdatedActionsGraph(_ actionsGraph: StatsGraph) -> GroupStats { public func withUpdatedActionsGraph(_ actionsGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: actionsGraph, topHoursGraph: self.topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
public func withUpdatedTopHoursGraph(_ topHoursGraph: StatsGraph) -> GroupStats { public func withUpdatedTopHoursGraph(_ topHoursGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: topHoursGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters) return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: topHoursGraph, topWeekdaysGraph: self.topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
}
public func withUpdatedTopWeekdaysGraph(_ topWeekdaysGraph: StatsGraph) -> GroupStats {
return GroupStats(period: self.period, members: self.members, messages: self.messages, viewers: self.viewers, posters: self.posters, growthGraph: self.growthGraph, membersGraph: self.membersGraph, newMembersBySourceGraph: self.newMembersBySourceGraph, languagesGraph: self.languagesGraph, messagesGraph: self.messagesGraph, actionsGraph: self.actionsGraph, topHoursGraph: self.topHoursGraph, topWeekdaysGraph: topWeekdaysGraph, topPosters: self.topPosters, topAdmins: self.topAdmins, topInviters: self.topInviters)
} }
} }
@ -684,7 +693,7 @@ private func requestGroupStats(postbox: Postbox, network: Network, datacenterId:
return signal return signal
|> mapToSignal { result -> Signal<GroupStats?, MTRpcError> in |> mapToSignal { result -> Signal<GroupStats?, MTRpcError> in
return postbox.transaction { transaction -> GroupStats? in return postbox.transaction { transaction -> GroupStats? in
if case let .megagroupStats(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, users) = result { if case let .megagroupStats(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, users) = result {
var parsedUsers: [Peer] = [] var parsedUsers: [Peer] = []
for user in users { for user in users {
parsedUsers.append(TelegramUser(user: user)) parsedUsers.append(TelegramUser(user: user))
@ -858,6 +867,21 @@ private final class GroupStatsContextImpl {
} }
} }
func loadTopWeekdaysGraph() {
guard let stats = self._state.stats else {
return
}
if case let .OnDemand(token) = stats.topWeekdaysGraph {
self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token)
|> deliverOnMainQueue).start(next: { [weak self] graph in
if let strongSelf = self, let graph = graph {
strongSelf._state = GroupStatsContextState(stats: strongSelf._state.stats?.withUpdatedTopWeekdaysGraph(graph))
strongSelf._statePromise.set(.single(strongSelf._state))
}
}), forKey: token)
}
}
func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> { func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
if let token = graph.token { if let token = graph.token {
return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x) return requestGraph(network: self.network, datacenterId: self.datacenterId, token: token, x: x)
@ -930,6 +954,12 @@ public final class GroupStatsContext {
} }
} }
public func loadTopWeekdaysGraph() {
self.impl.with { impl in
impl.loadTopWeekdaysGraph()
}
}
public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> { public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal<StatsGraph?, NoError> {
return Signal { subscriber in return Signal { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()
@ -973,7 +1003,11 @@ extension StatsGraph {
case let .statsGraphError(error): case let .statsGraphError(error):
self = .Failed(error: error) self = .Failed(error: error)
case let .statsGraphAsync(token): case let .statsGraphAsync(token):
self = .OnDemand(token: token) if !token.isEmpty {
self = .OnDemand(token: token)
} else {
self = .Failed(error: "An error occured. Please try again later.")
}
} }
} }
} }
@ -1056,11 +1090,11 @@ extension GroupStatsTopInviter {
extension GroupStats { extension GroupStats {
convenience init(apiMegagroupStats: Api.stats.MegagroupStats) { convenience init(apiMegagroupStats: Api.stats.MegagroupStats) {
switch apiMegagroupStats { switch apiMegagroupStats {
case let .megagroupStats(period, members, messages, viewers, posters, apiGrowthGraph, apiMembersGraph, apiNewMembersBySourceGraph, apiLanguagesGraph, apiMessagesGraph, apiActionsGraph, apiTopHoursGraph, topPosters, topAdmins, topInviters, users): case let .megagroupStats(period, members, messages, viewers, posters, apiGrowthGraph, apiMembersGraph, apiNewMembersBySourceGraph, apiLanguagesGraph, apiMessagesGraph, apiActionsGraph, apiTopHoursGraph, apiTopWeekdaysGraph, topPosters, topAdmins, topInviters, users):
let growthGraph = StatsGraph(apiStatsGraph: apiGrowthGraph) let growthGraph = StatsGraph(apiStatsGraph: apiGrowthGraph)
let isEmpty = growthGraph.isEmpty let isEmpty = growthGraph.isEmpty
self.init(period: StatsDateRange(apiStatsDateRangeDays: period), members: StatsValue(apiStatsAbsValueAndPrev: members), messages: StatsValue(apiStatsAbsValueAndPrev: messages), viewers: StatsValue(apiStatsAbsValueAndPrev: viewers), posters: StatsValue(apiStatsAbsValueAndPrev: posters), growthGraph: growthGraph, membersGraph: StatsGraph(apiStatsGraph: apiMembersGraph), newMembersBySourceGraph: StatsGraph(apiStatsGraph: apiNewMembersBySourceGraph), languagesGraph: StatsGraph(apiStatsGraph: apiLanguagesGraph), messagesGraph: StatsGraph(apiStatsGraph: apiMessagesGraph), actionsGraph: StatsGraph(apiStatsGraph: apiActionsGraph), topHoursGraph: StatsGraph(apiStatsGraph: apiTopHoursGraph), topPosters: topPosters.map { GroupStatsTopPoster(apiStatsGroupTopPoster: $0) }, topAdmins: topAdmins.map { GroupStatsTopAdmin(apiStatsGroupTopAdmin: $0) }, topInviters: topInviters.map { GroupStatsTopInviter(apiStatsGroupTopInviter: $0) }) self.init(period: StatsDateRange(apiStatsDateRangeDays: period), members: StatsValue(apiStatsAbsValueAndPrev: members), messages: StatsValue(apiStatsAbsValueAndPrev: messages), viewers: StatsValue(apiStatsAbsValueAndPrev: viewers), posters: StatsValue(apiStatsAbsValueAndPrev: posters), growthGraph: growthGraph, membersGraph: StatsGraph(apiStatsGraph: apiMembersGraph), newMembersBySourceGraph: StatsGraph(apiStatsGraph: apiNewMembersBySourceGraph), languagesGraph: StatsGraph(apiStatsGraph: apiLanguagesGraph), messagesGraph: StatsGraph(apiStatsGraph: apiMessagesGraph), actionsGraph: StatsGraph(apiStatsGraph: apiActionsGraph), topHoursGraph: StatsGraph(apiStatsGraph: apiTopHoursGraph), topWeekdaysGraph: StatsGraph(apiStatsGraph: apiTopWeekdaysGraph), topPosters: topPosters.map { GroupStatsTopPoster(apiStatsGroupTopPoster: $0) }, topAdmins: topAdmins.map { GroupStatsTopAdmin(apiStatsGroupTopAdmin: $0) }, topInviters: topInviters.map { GroupStatsTopInviter(apiStatsGroupTopInviter: $0) })
} }
} }
} }

View File

@ -314,7 +314,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var isDismissed = false private var isDismissed = false
private var focusOnSearchAfterAppearance: Bool = false private var focusOnSearchAfterAppearance: (ChatSearchDomain, String)?
private let keepPeerInfoScreenDataHotDisposable = MetaDisposable() private let keepPeerInfoScreenDataHotDisposable = MetaDisposable()
@ -2550,7 +2550,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
$0.updatedInputTextPanelState { panelState in $0.updatedInputTextPanelState { panelState in
if let videoRecorder = videoRecorder { if let videoRecorder = videoRecorder {
if panelState.mediaRecordingState == nil { if panelState.mediaRecordingState == nil {
return panelState.withUpdatedMediaRecordingState(.video(status: .recording(videoRecorder.audioStatus), isLocked: false)) return panelState.withUpdatedMediaRecordingState(.video(status: .recording(videoRecorder.audioStatus), isLocked: strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId))
} }
} else { } else {
return panelState.withUpdatedMediaRecordingState(nil) return panelState.withUpdatedMediaRecordingState(nil)
@ -2562,12 +2562,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let videoRecorder = videoRecorder { if let videoRecorder = videoRecorder {
strongSelf.recorderFeedback?.impact(.light) strongSelf.recorderFeedback?.impact(.light)
videoRecorder.onDismiss = { videoRecorder.onDismiss = { [weak self] isCancelled in
if let strongSelf = self { self?.chatDisplayNode.updateRecordedMediaDeleted(isCancelled)
strongSelf.beginMediaRecordingRequestId += 1 self?.beginMediaRecordingRequestId += 1
strongSelf.lockMediaRecordingRequestId = nil self?.lockMediaRecordingRequestId = nil
strongSelf.videoRecorder.set(.single(nil)) self?.videoRecorder.set(.single(nil))
}
} }
videoRecorder.onStop = { videoRecorder.onStop = {
if let strongSelf = self { if let strongSelf = self {
@ -4738,9 +4737,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.interfaceInteraction = interfaceInteraction self.interfaceInteraction = interfaceInteraction
if self.focusOnSearchAfterAppearance { if let search = self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = false self.focusOnSearchAfterAppearance = nil
self.interfaceInteraction?.beginMessageSearch(.everything, "") self.interfaceInteraction?.beginMessageSearch(search.0, search.1)
} }
self.chatDisplayNode.interfaceInteraction = interfaceInteraction self.chatDisplayNode.interfaceInteraction = interfaceInteraction
@ -5005,8 +5004,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
if self.focusOnSearchAfterAppearance { if let _ = self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = false self.focusOnSearchAfterAppearance = nil
if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode { if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode {
searchNode.activate() searchNode.activate()
} }
@ -9253,9 +9252,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
func activateSearch() { func activateSearch(domain: ChatSearchDomain = .everything, query: String = "") {
self.focusOnSearchAfterAppearance = true self.focusOnSearchAfterAppearance = (domain, query)
self.interfaceInteraction?.beginMessageSearch(.everything, "") self.interfaceInteraction?.beginMessageSearch(domain, query)
} }
} }

View File

@ -128,35 +128,6 @@ struct ChatSearchResultsState: Equatable {
let totalCount: Int32 let totalCount: Int32
let completed: Bool let completed: Bool
} }
enum ChatSearchDomain: Equatable {
case everything
case members
case member(Peer)
static func ==(lhs: ChatSearchDomain, rhs: ChatSearchDomain) -> Bool {
switch lhs {
case .everything:
if case .everything = rhs {
return true
} else {
return false
}
case .members:
if case .members = rhs {
return true
} else {
return false
}
case let .member(lhsPeer):
if case let .member(rhsPeer) = rhs, lhsPeer.isEqual(rhsPeer) {
return true
} else {
return false
}
}
}
}
enum ChatSearchDomainSuggestionContext: Equatable { enum ChatSearchDomainSuggestionContext: Equatable {
case none case none

View File

@ -1011,9 +1011,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if !deltaOffset.isZero { if !deltaOffset.isZero {
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: deltaOffset, y: 0.0), to: CGPoint(), duration: 0.3, additive: true) audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: deltaOffset, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
} }
} else if audioRecordingCancelIndicator.layer.animation(forKey: "slide_juggle") == nil { } else if audioRecordingCancelIndicator.layer.animation(forKey: "slide_juggle") == nil, baseWidth > 320 {
let slideJuggleAnimation = CABasicAnimation(keyPath: "transform") let slideJuggleAnimation = CABasicAnimation(keyPath: "transform")
slideJuggleAnimation.toValue = CATransform3DMakeTranslation(-6, 0, 0) slideJuggleAnimation.toValue = CATransform3DMakeTranslation(6, 0, 0)
slideJuggleAnimation.duration = 1 slideJuggleAnimation.duration = 1
slideJuggleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) slideJuggleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
slideJuggleAnimation.autoreverses = true slideJuggleAnimation.autoreverses = true
@ -1023,11 +1023,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0)) let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0))
let cancelMinX = audioRecordingCancelIndicator.alpha > 0.5 ? audioRecordingCancelIndicator.frame.minX : width
audioRecordingInfoContainerNode.frame = CGRect( audioRecordingInfoContainerNode.frame = CGRect(
origin: CGPoint( origin: CGPoint(
x: min(leftInset, cancelMinX - audioRecordingTimeSize.width - 8.0 - 28.0), x: min(leftInset, width - audioRecordingTimeSize.width - 8.0 - 28.0),
y: 0.0 y: 0.0
), ),
size: CGSize(width: baseWidth, height: panelHeight) size: CGSize(width: baseWidth, height: panelHeight)
@ -1051,7 +1049,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.addSubnode(audioRecordingDotNode) self.addSubnode(audioRecordingDotNode)
} }
animateDotAppearing = transition.isAnimated && !isLocked && !hideInfo animateDotAppearing = transition.isAnimated && !hideInfo
audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - 44 + 1), size: CGSize(width: 40.0, height: 40)) audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - 44 + 1), size: CGSize(width: 40.0, height: 40))
if animateDotAppearing { if animateDotAppearing {
@ -1074,9 +1072,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
} }
if hideInfo { if hideInfo {
audioRecordingDotNode.layer.animateAlpha(from: audioRecordingDotNode.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) audioRecordingDotNode.layer.removeAllAnimations()
audioRecordingTimeNode.layer.animateAlpha(from: audioRecordingTimeNode.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) audioRecordingTimeNode.layer.animateAlpha(from: CGFloat(audioRecordingTimeNode.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingCancelIndicator.layer.animateAlpha(from: CGFloat(audioRecordingCancelIndicator.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
} }
} else { } else {
self.actionButtons.micButton.audioRecorder = nil self.actionButtons.micButton.audioRecorder = nil
@ -1091,8 +1090,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
self.audioRecordingInfoContainerNode = nil self.audioRecordingInfoContainerNode = nil
//transition.updateTransformScale(node: audioRecordingInfoContainerNode, scale: 0)
//transition.updatePosition(node: audioRecordingInfoContainerNode, position: CGPoint(x: audioRecordingInfoContainerNode.position.x - 10, y: audioRecordingInfoContainerNode.position.y))
transition.updateAlpha(node: audioRecordingInfoContainerNode, alpha: 0) { [weak audioRecordingInfoContainerNode] _ in transition.updateAlpha(node: audioRecordingInfoContainerNode, alpha: 0) { [weak audioRecordingInfoContainerNode] _ in
audioRecordingInfoContainerNode?.removeFromSupernode() audioRecordingInfoContainerNode?.removeFromSupernode()
} }
@ -1104,7 +1101,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self?.audioRecordingDotNode = nil self?.audioRecordingDotNode = nil
audioRecordingDotNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0, removeOnCompletion: false) audioRecordingDotNode.layer.animateScale(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), to: 0.3, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingDotNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0, removeOnCompletion: false) { [weak audioRecordingDotNode] _ in audioRecordingDotNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0, removeOnCompletion: false) { [weak audioRecordingDotNode] _ in
audioRecordingDotNode?.removeFromSupernode() audioRecordingDotNode?.removeFromSupernode()
} }

View File

@ -29,7 +29,7 @@ final class InstantVideoControllerRecordingStatus {
final class InstantVideoController: LegacyController, StandalonePresentableController { final class InstantVideoController: LegacyController, StandalonePresentableController {
private var captureController: TGVideoMessageCaptureController? private var captureController: TGVideoMessageCaptureController?
var onDismiss: (() -> Void)? var onDismiss: ((Bool) -> Void)?
var onStop: (() -> Void)? var onStop: (() -> Void)?
private let micLevelValue = ValuePromise<Float>(0.0) private let micLevelValue = ValuePromise<Float>(0.0)
@ -59,8 +59,8 @@ final class InstantVideoController: LegacyController, StandalonePresentableContr
captureController.onDuration = { [weak self] duration in captureController.onDuration = { [weak self] duration in
self?.durationValue.set(duration) self?.durationValue.set(duration)
} }
captureController.onDismiss = { [weak self] _ in captureController.onDismiss = { [weak self] _, isCancelled in
self?.onDismiss?() self?.onDismiss?(isCancelled)
} }
captureController.onStop = { [weak self] in captureController.onStop = { [weak self] in
self?.onStop?() self?.onStop?()

View File

@ -34,8 +34,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
controller.scrollToEndOfHistory() controller.scrollToEndOfHistory()
let _ = params.navigationController.popToViewController(controller, animated: params.animated) let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller) params.completion(controller)
} else if params.activateMessageSearch { } else if let search = params.activateMessageSearch {
controller.activateSearch() controller.activateSearch(domain: search.0, query: search.1)
let _ = params.navigationController.popToViewController(controller, animated: params.animated) let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller) params.completion(controller)
} else { } else {
@ -70,8 +70,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData) controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData)
} }
controller.purposefulAction = params.purposefulAction controller.purposefulAction = params.purposefulAction
if params.activateMessageSearch { if let search = params.activateMessageSearch {
controller.activateSearch() controller.activateSearch(domain: search.0, query: search.1)
} }
let resolvedKeepStack: Bool let resolvedKeepStack: Bool
switch params.keepStack { switch params.keepStack {

View File

@ -2410,7 +2410,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
private func openChatWithMessageSearch() { private func openChatWithMessageSearch() {
if let navigationController = (self.controller?.navigationController as? NavigationController) { if let navigationController = (self.controller?.navigationController as? NavigationController) {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), activateMessageSearch: true)) self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), activateMessageSearch: (.everything, "")))
} }
} }

View File

@ -597,7 +597,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
if let strongSelf = self { if let strongSelf = self {
let resolvedText: CallStatusText let resolvedText: CallStatusText
if let state = state { if let state = state {
switch state { switch state.state {
case .connecting, .requesting, .terminating, .ringing, .waiting: case .connecting, .requesting, .terminating, .ringing, .waiting:
resolvedText = .inProgress(nil) resolvedText = .inProgress(nil)
case .terminated: case .terminated:
@ -1014,6 +1014,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller return controller
} }
public func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController? {
let controller = channelAdminController(context: context, peerId: peerId, adminId: adminId, initialParticipant: initialParticipant, updated: { _ in }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
return controller
}
public func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { public func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) {
openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput)
} }

View File

@ -496,7 +496,7 @@ public final class SharedNotificationManager {
if isIntegratedWithCallKit { if isIntegratedWithCallKit {
return nil return nil
} }
if case .ringing = state { if case .ringing = state.state {
return (peer, internalId) return (peer, internalId)
} else { } else {
return nil return nil

View File

@ -93,11 +93,20 @@ private let setupLogs: Bool = {
return true return true
}() }()
public enum OngoingCallContextState { public struct OngoingCallContextState: Equatable {
case initializing public enum State {
case connected case initializing
case reconnecting case connected
case failed case reconnecting
case failed
}
public enum VideoState: Equatable {
case notAvailable
case available(Bool)
case active
}
public let state: State
public let videoState: VideoState
} }
private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ { private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ {
@ -226,6 +235,8 @@ private func ongoingDataSavingForTypeWebrtc(_ type: VoiceCallDataSaving) -> Ongo
private protocol OngoingCallThreadLocalContextProtocol: class { private protocol OngoingCallThreadLocalContextProtocol: class {
func nativeSetNetworkType(_ type: NetworkType) func nativeSetNetworkType(_ type: NetworkType)
func nativeSetIsMuted(_ value: Bool) func nativeSetIsMuted(_ value: Bool)
func nativeSetVideoEnabled(_ value: Bool)
func nativeSwitchVideoCamera()
func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void)
func nativeDebugInfo() -> String func nativeDebugInfo() -> String
func nativeVersion() -> String func nativeVersion() -> String
@ -253,6 +264,12 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol {
self.setIsMuted(value) self.setIsMuted(value)
} }
func nativeSetVideoEnabled(_ value: Bool) {
}
func nativeSwitchVideoCamera() {
}
func nativeDebugInfo() -> String { func nativeDebugInfo() -> String {
return self.debugInfo() ?? "" return self.debugInfo() ?? ""
} }
@ -279,6 +296,14 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt
self.setIsMuted(value) self.setIsMuted(value)
} }
func nativeSetVideoEnabled(_ value: Bool) {
self.setVideoEnabled(value)
}
func nativeSwitchVideoCamera() {
self.switchVideoCamera()
}
func nativeDebugInfo() -> String { func nativeDebugInfo() -> String {
return self.debugInfo() ?? "" return self.debugInfo() ?? ""
} }
@ -318,7 +343,7 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt
} }
}*/ }*/
private extension OngoingCallContextState { private extension OngoingCallContextState.State {
init(_ state: OngoingCallState) { init(_ state: OngoingCallState) {
switch state { switch state {
case .initializing: case .initializing:
@ -335,7 +360,7 @@ private extension OngoingCallContextState {
} }
} }
private extension OngoingCallContextState { private extension OngoingCallContextState.State {
init(_ state: OngoingCallStateWebrtc) { init(_ state: OngoingCallStateWebrtc) {
switch state { switch state {
case .initializing: case .initializing:
@ -471,8 +496,25 @@ public final class OngoingCallContext {
}) })
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
context.stateChanged = { state in context.stateChanged = { state, videoState in
self?.contextState.set(.single(OngoingCallContextState(state))) queue.async {
guard let strongSelf = self else {
return
}
let mappedState = OngoingCallContextState.State(state)
let mappedVideoState: OngoingCallContextState.VideoState
switch videoState {
case .inactive:
mappedVideoState = .available(true)
case .active:
mappedVideoState = .active
case .invited, .requesting:
mappedVideoState = .available(false)
@unknown default:
mappedVideoState = .available(false)
}
strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState)))
}
} }
context.signalBarsChanged = { signalBars in context.signalBarsChanged = { signalBars in
self?.receptionPromise.set(.single(signalBars)) self?.receptionPromise.set(.single(signalBars))
@ -498,7 +540,7 @@ public final class OngoingCallContext {
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
context.stateChanged = { state in context.stateChanged = { state in
self?.contextState.set(.single(OngoingCallContextState(state))) self?.contextState.set(.single(OngoingCallContextState(state: OngoingCallContextState.State(state), videoState: .notAvailable)))
} }
context.signalBarsChanged = { signalBars in context.signalBarsChanged = { signalBars in
self?.receptionPromise.set(.single(signalBars)) self?.receptionPromise.set(.single(signalBars))
@ -588,6 +630,18 @@ public final class OngoingCallContext {
} }
} }
public func setEnableVideo(_ value: Bool) {
self.withContext { context in
context.nativeSetVideoEnabled(value)
}
}
public func switchVideoCamera() {
self.withContext { context in
context.nativeSwitchVideoCamera()
}
}
public func debugInfo() -> Signal<(String, String), NoError> { public func debugInfo() -> Signal<(String, String), NoError> {
let poll = Signal<(String, String), NoError> { subscriber in let poll = Signal<(String, String), NoError> { subscriber in
self.withContext { context in self.withContext { context in

View File

@ -29,6 +29,7 @@ objc_library(
"-Ithird-party/webrtc/webrtc-ios/src/sdk/objc/components/renderer/metal", "-Ithird-party/webrtc/webrtc-ios/src/sdk/objc/components/renderer/metal",
"-Ithird-party/webrtc/webrtc-ios/src/sdk/objc/components/video_codec", "-Ithird-party/webrtc/webrtc-ios/src/sdk/objc/components/video_codec",
"-Ithird-party/webrtc/webrtc-ios/src/third_party/libyuv/include", "-Ithird-party/webrtc/webrtc-ios/src/third_party/libyuv/include",
"-Ithird-party/webrtc/webrtc-ios/src/sdk/objc/api/video_codec",
"-DWEBRTC_IOS", "-DWEBRTC_IOS",
"-DWEBRTC_MAC", "-DWEBRTC_MAC",
"-DWEBRTC_POSIX", "-DWEBRTC_POSIX",

View File

@ -18,8 +18,9 @@ public:
void configurePlatformAudio(); void configurePlatformAudio();
std::unique_ptr<webrtc::VideoEncoderFactory> makeVideoEncoderFactory(); std::unique_ptr<webrtc::VideoEncoderFactory> makeVideoEncoderFactory();
std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory(); std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory();
bool supportsH265Encoding();
rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread); rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread);
std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source); std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera);
#ifdef TGVOIP_NAMESPACE #ifdef TGVOIP_NAMESPACE
} }

View File

@ -30,6 +30,8 @@
#import "VideoCameraCapturer.h" #import "VideoCameraCapturer.h"
#import <AVFoundation/AVFoundation.h>
@interface VideoCapturerInterfaceImplReference : NSObject { @interface VideoCapturerInterfaceImplReference : NSObject {
VideoCameraCapturer *_videoCapturer; VideoCameraCapturer *_videoCapturer;
} }
@ -38,7 +40,7 @@
@implementation VideoCapturerInterfaceImplReference @implementation VideoCapturerInterfaceImplReference
- (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source { - (instancetype)initWithSource:(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface>)source useFrontCamera:(bool)useFrontCamera {
self = [super init]; self = [super init];
if (self != nil) { if (self != nil) {
assert([NSThread isMainThread]); assert([NSThread isMainThread]);
@ -46,18 +48,27 @@
_videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source]; _videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source];
AVCaptureDevice *frontCamera = nil; AVCaptureDevice *frontCamera = nil;
AVCaptureDevice *backCamera = nil;
for (AVCaptureDevice *device in [VideoCameraCapturer captureDevices]) { for (AVCaptureDevice *device in [VideoCameraCapturer captureDevices]) {
if (device.position == AVCaptureDevicePositionFront) { if (device.position == AVCaptureDevicePositionFront) {
frontCamera = device; frontCamera = device;
break; } else if (device.position == AVCaptureDevicePositionBack) {
backCamera = device;
} }
} }
if (frontCamera == nil) { AVCaptureDevice *selectedCamera = nil;
if (useFrontCamera && frontCamera != nil) {
selectedCamera = frontCamera;
} else {
selectedCamera = backCamera;
}
if (selectedCamera == nil) {
return nil; return nil;
} }
NSArray<AVCaptureDeviceFormat *> *sortedFormats = [[VideoCameraCapturer supportedFormatsForDevice:frontCamera] sortedArrayUsingComparator:^NSComparisonResult(AVCaptureDeviceFormat* lhs, AVCaptureDeviceFormat *rhs) { NSArray<AVCaptureDeviceFormat *> *sortedFormats = [[VideoCameraCapturer supportedFormatsForDevice:selectedCamera] sortedArrayUsingComparator:^NSComparisonResult(AVCaptureDeviceFormat* lhs, AVCaptureDeviceFormat *rhs) {
int32_t width1 = CMVideoFormatDescriptionGetDimensions(lhs.formatDescription).width; int32_t width1 = CMVideoFormatDescriptionGetDimensions(lhs.formatDescription).width;
int32_t width2 = CMVideoFormatDescriptionGetDimensions(rhs.formatDescription).width; int32_t width2 = CMVideoFormatDescriptionGetDimensions(rhs.formatDescription).width;
return width1 < width2 ? NSOrderedAscending : NSOrderedDescending; return width1 < width2 ? NSOrderedAscending : NSOrderedDescending;
@ -90,7 +101,7 @@
return nil; return nil;
} }
[_videoCapturer startCaptureWithDevice:frontCamera format:bestFormat fps:30]; [_videoCapturer startCaptureWithDevice:selectedCamera format:bestFormat fps:30];
} }
return self; return self;
} }
@ -119,12 +130,12 @@ namespace TGVOIP_NAMESPACE {
class VideoCapturerInterfaceImpl: public VideoCapturerInterface { class VideoCapturerInterfaceImpl: public VideoCapturerInterface {
public: public:
VideoCapturerInterfaceImpl(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source) : VideoCapturerInterfaceImpl(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera) :
_source(source) { _source(source) {
_implReference = [[VideoCapturerInterfaceImplHolder alloc] init]; _implReference = [[VideoCapturerInterfaceImplHolder alloc] init];
VideoCapturerInterfaceImplHolder *implReference = _implReference; VideoCapturerInterfaceImplHolder *implReference = _implReference;
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
VideoCapturerInterfaceImplReference *value = [[VideoCapturerInterfaceImplReference alloc] initWithSource:source]; VideoCapturerInterfaceImplReference *value = [[VideoCapturerInterfaceImplReference alloc] initWithSource:source useFrontCamera:useFrontCamera];
if (value != nil) { if (value != nil) {
implReference.reference = (void *)CFBridgingRetain(value); implReference.reference = (void *)CFBridgingRetain(value);
} }
@ -161,13 +172,21 @@ std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory() {
return webrtc::ObjCToNativeVideoDecoderFactory([[TGRTCDefaultVideoDecoderFactory alloc] init]); return webrtc::ObjCToNativeVideoDecoderFactory([[TGRTCDefaultVideoDecoderFactory alloc] init]);
} }
bool supportsH265Encoding() {
if (@available(iOS 11.0, *)) {
return [[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality];
} else {
return false;
}
}
rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread) { rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread) {
rtc::scoped_refptr<webrtc::ObjCVideoTrackSource> objCVideoTrackSource(new rtc::RefCountedObject<webrtc::ObjCVideoTrackSource>()); rtc::scoped_refptr<webrtc::ObjCVideoTrackSource> objCVideoTrackSource(new rtc::RefCountedObject<webrtc::ObjCVideoTrackSource>());
return webrtc::VideoTrackSourceProxy::Create(signalingThread, workerThread, objCVideoTrackSource); return webrtc::VideoTrackSourceProxy::Create(signalingThread, workerThread, objCVideoTrackSource);
} }
std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source) { std::unique_ptr<VideoCapturerInterface> makeVideoCapturer(rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> source, bool useFrontCamera) {
return std::make_unique<VideoCapturerInterfaceImpl>(source); return std::make_unique<VideoCapturerInterfaceImpl>(source, useFrontCamera);
} }
#ifdef TGVOIP_NAMESPACE #ifdef TGVOIP_NAMESPACE

View File

@ -1,5 +1,7 @@
#include "Manager.h" #include "Manager.h"
#include "rtc_base/byte_buffer.h"
#ifdef TGVOIP_NAMESPACE #ifdef TGVOIP_NAMESPACE
namespace TGVOIP_NAMESPACE { namespace TGVOIP_NAMESPACE {
#endif #endif
@ -35,13 +37,16 @@ Manager::Manager(
TgVoipEncryptionKey encryptionKey, TgVoipEncryptionKey encryptionKey,
bool enableP2P, bool enableP2P,
std::function<void (const TgVoipState &)> stateUpdated, std::function<void (const TgVoipState &)> stateUpdated,
std::function<void (bool)> videoStateUpdated,
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
) : ) :
_thread(thread), _thread(thread),
_encryptionKey(encryptionKey), _encryptionKey(encryptionKey),
_enableP2P(enableP2P), _enableP2P(enableP2P),
_stateUpdated(stateUpdated), _stateUpdated(stateUpdated),
_signalingDataEmitted(signalingDataEmitted) { _videoStateUpdated(videoStateUpdated),
_signalingDataEmitted(signalingDataEmitted),
_isVideoRequested(false) {
assert(_thread->IsCurrent()); assert(_thread->IsCurrent());
} }
@ -87,7 +92,14 @@ void Manager::start() {
}); });
}, },
[signalingDataEmitted](const std::vector<uint8_t> &data) { [signalingDataEmitted](const std::vector<uint8_t> &data) {
signalingDataEmitted(data); rtc::CopyOnWriteBuffer buffer;
uint8_t mode = 3;
buffer.AppendData(&mode, 1);
buffer.AppendData(data.data(), data.size());
std::vector<uint8_t> augmentedData;
augmentedData.resize(buffer.size());
memcpy(augmentedData.data(), buffer.data(), buffer.size());
signalingDataEmitted(augmentedData);
} }
); );
})); }));
@ -112,8 +124,59 @@ void Manager::start() {
} }
void Manager::receiveSignalingData(const std::vector<uint8_t> &data) { void Manager::receiveSignalingData(const std::vector<uint8_t> &data) {
_networkManager->perform([data](NetworkManager *networkManager) { rtc::CopyOnWriteBuffer buffer;
networkManager->receiveSignalingData(data); buffer.AppendData(data.data(), data.size());
if (buffer.size() < 1) {
return;
}
rtc::ByteBufferReader reader((const char *)buffer.data(), buffer.size());
uint8_t mode = 0;
if (!reader.ReadUInt8(&mode)) {
return;
}
if (mode == 1) {
_mediaManager->perform([](MediaManager *mediaManager) {
mediaManager->setSendVideo(true);
});
_videoStateUpdated(true);
} else if (mode == 2) {
} else if (mode == 3) {
auto candidatesData = buffer.Slice(1, buffer.size() - 1);
_networkManager->perform([candidatesData](NetworkManager *networkManager) {
networkManager->receiveSignalingData(candidatesData);
});
}
}
void Manager::setSendVideo(bool sendVideo) {
if (sendVideo) {
if (!_isVideoRequested) {
_isVideoRequested = true;
rtc::CopyOnWriteBuffer buffer;
uint8_t mode = 1;
buffer.AppendData(&mode, 1);
std::vector<uint8_t> data;
data.resize(buffer.size());
memcpy(data.data(), buffer.data(), buffer.size());
_signalingDataEmitted(data);
_mediaManager->perform([](MediaManager *mediaManager) {
mediaManager->setSendVideo(true);
});
_videoStateUpdated(true);
}
}
}
void Manager::switchVideoCamera() {
_mediaManager->perform([](MediaManager *mediaManager) {
mediaManager->switchVideoCamera();
}); });
} }

View File

@ -17,12 +17,15 @@ public:
TgVoipEncryptionKey encryptionKey, TgVoipEncryptionKey encryptionKey,
bool enableP2P, bool enableP2P,
std::function<void (const TgVoipState &)> stateUpdated, std::function<void (const TgVoipState &)> stateUpdated,
std::function<void (bool)> videoStateUpdated,
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
); );
~Manager(); ~Manager();
void start(); void start();
void receiveSignalingData(const std::vector<uint8_t> &data); void receiveSignalingData(const std::vector<uint8_t> &data);
void setSendVideo(bool sendVideo);
void switchVideoCamera();
void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink); void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
void setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink); void setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
@ -31,9 +34,11 @@ private:
TgVoipEncryptionKey _encryptionKey; TgVoipEncryptionKey _encryptionKey;
bool _enableP2P; bool _enableP2P;
std::function<void (const TgVoipState &)> _stateUpdated; std::function<void (const TgVoipState &)> _stateUpdated;
std::function<void (bool)> _videoStateUpdated;
std::function<void (const std::vector<uint8_t> &)> _signalingDataEmitted; std::function<void (const std::vector<uint8_t> &)> _signalingDataEmitted;
std::unique_ptr<ThreadLocalObject<NetworkManager>> _networkManager; std::unique_ptr<ThreadLocalObject<NetworkManager>> _networkManager;
std::unique_ptr<ThreadLocalObject<MediaManager>> _mediaManager; std::unique_ptr<ThreadLocalObject<MediaManager>> _mediaManager;
bool _isVideoRequested;
private: private:
}; };

View File

@ -17,6 +17,8 @@
#include "api/video/video_bitrate_allocation.h" #include "api/video/video_bitrate_allocation.h"
#include "call/call.h" #include "call/call.h"
#include "api/video_codecs/builtin_video_encoder_factory.h"
#if TARGET_OS_IPHONE #if TARGET_OS_IPHONE
#include "CodecsApple.h" #include "CodecsApple.h"
@ -108,27 +110,50 @@ static std::vector<cricket::VideoCodec> AssignPayloadTypesAndDefaultCodecs(std::
return output_codecs; return output_codecs;
} }
static int sendCodecPriority(const cricket::VideoCodec &codec) {
int priotity = 0;
if (codec.name == cricket::kAv1CodecName) {
return priotity;
}
priotity++;
if (codec.name == cricket::kH265CodecName) {
if (supportsH265Encoding()) {
return priotity;
}
}
priotity++;
if (codec.name == cricket::kH264CodecName) {
return priotity;
}
priotity++;
if (codec.name == cricket::kVp9CodecName) {
return priotity;
}
priotity++;
if (codec.name == cricket::kVp8CodecName) {
return priotity;
}
priotity++;
return -1;
}
static absl::optional<cricket::VideoCodec> selectVideoCodec(std::vector<cricket::VideoCodec> &codecs) { static absl::optional<cricket::VideoCodec> selectVideoCodec(std::vector<cricket::VideoCodec> &codecs) {
bool useVP9 = false; std::vector<cricket::VideoCodec> sortedCodecs;
bool useH265 = true;
for (auto &codec : codecs) { for (auto &codec : codecs) {
if (useVP9) { if (sendCodecPriority(codec) != -1) {
if (codec.name == cricket::kVp9CodecName) { sortedCodecs.push_back(codec);
return absl::optional<cricket::VideoCodec>(codec);
}
} else if (useH265) {
if (codec.name == cricket::kH265CodecName) {
return absl::optional<cricket::VideoCodec>(codec);
}
} else {
if (codec.name == cricket::kH264CodecName) {
return absl::optional<cricket::VideoCodec>(codec);
}
} }
} }
return absl::optional<cricket::VideoCodec>(); std::sort(sortedCodecs.begin(), sortedCodecs.end(), [](const cricket::VideoCodec &lhs, const cricket::VideoCodec &rhs) {
return sendCodecPriority(lhs) < sendCodecPriority(rhs);
});
if (sortedCodecs.size() != 0) {
return sortedCodecs[0];
} else {
return absl::nullopt;
}
} }
static rtc::Thread *makeWorkerThread() { static rtc::Thread *makeWorkerThread() {
@ -162,6 +187,14 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
_ssrcVideo.fecIncoming = isOutgoing ? ssrcVideoFecIncoming : ssrcVideoFecOutgoing; _ssrcVideo.fecIncoming = isOutgoing ? ssrcVideoFecIncoming : ssrcVideoFecOutgoing;
_ssrcVideo.fecOutgoing = (!isOutgoing) ? ssrcVideoFecIncoming : ssrcVideoFecOutgoing; _ssrcVideo.fecOutgoing = (!isOutgoing) ? ssrcVideoFecIncoming : ssrcVideoFecOutgoing;
_isConnected = false;
auto videoEncoderFactory = makeVideoEncoderFactory();
_videoCodecs = AssignPayloadTypesAndDefaultCodecs(videoEncoderFactory->GetSupportedFormats());
_isSendingVideo = false;
_useFrontCamera = true;
_audioNetworkInterface = std::unique_ptr<MediaManager::NetworkInterfaceImpl>(new MediaManager::NetworkInterfaceImpl(this, false)); _audioNetworkInterface = std::unique_ptr<MediaManager::NetworkInterfaceImpl>(new MediaManager::NetworkInterfaceImpl(this, false));
_videoNetworkInterface = std::unique_ptr<MediaManager::NetworkInterfaceImpl>(new MediaManager::NetworkInterfaceImpl(this, true)); _videoNetworkInterface = std::unique_ptr<MediaManager::NetworkInterfaceImpl>(new MediaManager::NetworkInterfaceImpl(this, true));
@ -182,9 +215,6 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
mediaDeps.audio_encoder_factory = webrtc::CreateAudioEncoderFactory<webrtc::AudioEncoderOpus>(); mediaDeps.audio_encoder_factory = webrtc::CreateAudioEncoderFactory<webrtc::AudioEncoderOpus>();
mediaDeps.audio_decoder_factory = webrtc::CreateAudioDecoderFactory<webrtc::AudioDecoderOpus>(); mediaDeps.audio_decoder_factory = webrtc::CreateAudioDecoderFactory<webrtc::AudioDecoderOpus>();
auto videoEncoderFactory = makeVideoEncoderFactory();
std::vector<cricket::VideoCodec> videoCodecs = AssignPayloadTypesAndDefaultCodecs(videoEncoderFactory->GetSupportedFormats());
mediaDeps.video_encoder_factory = makeVideoEncoderFactory(); mediaDeps.video_encoder_factory = makeVideoEncoderFactory();
mediaDeps.video_decoder_factory = makeVideoDecoderFactory(); mediaDeps.video_decoder_factory = makeVideoDecoderFactory();
@ -211,7 +241,6 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
const uint8_t opusMaxBitrateKbps = 32; const uint8_t opusMaxBitrateKbps = 32;
const uint8_t opusStartBitrateKbps = 6; const uint8_t opusStartBitrateKbps = 6;
const uint8_t opusPTimeMs = 120; const uint8_t opusPTimeMs = 120;
const int extensionSequenceOne = 1;
cricket::AudioCodec opusCodec(opusSdpPayload, opusSdpName, opusClockrate, opusSdpBitrate, opusSdpChannels); cricket::AudioCodec opusCodec(opusSdpPayload, opusSdpName, opusClockrate, opusSdpBitrate, opusSdpChannels);
opusCodec.AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamTransportCc)); opusCodec.AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamTransportCc));
@ -223,7 +252,7 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
cricket::AudioSendParameters audioSendPrameters; cricket::AudioSendParameters audioSendPrameters;
audioSendPrameters.codecs.push_back(opusCodec); audioSendPrameters.codecs.push_back(opusCodec);
audioSendPrameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, extensionSequenceOne); audioSendPrameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, 1);
audioSendPrameters.options.echo_cancellation = false; audioSendPrameters.options.echo_cancellation = false;
//audioSendPrameters.options.experimental_ns = false; //audioSendPrameters.options.experimental_ns = false;
audioSendPrameters.options.noise_suppression = false; audioSendPrameters.options.noise_suppression = false;
@ -238,7 +267,7 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
cricket::AudioRecvParameters audioRecvParameters; cricket::AudioRecvParameters audioRecvParameters;
audioRecvParameters.codecs.emplace_back(opusSdpPayload, opusSdpName, opusClockrate, opusSdpBitrate, opusSdpChannels); audioRecvParameters.codecs.emplace_back(opusSdpPayload, opusSdpName, opusClockrate, opusSdpBitrate, opusSdpChannels);
audioRecvParameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, extensionSequenceOne); audioRecvParameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, 1);
audioRecvParameters.rtcp.reduced_size = true; audioRecvParameters.rtcp.reduced_size = true;
audioRecvParameters.rtcp.remote_estimate = true; audioRecvParameters.rtcp.remote_estimate = true;
@ -246,75 +275,9 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
_audioChannel->AddRecvStream(cricket::StreamParams::CreateLegacy(_ssrcAudio.incoming)); _audioChannel->AddRecvStream(cricket::StreamParams::CreateLegacy(_ssrcAudio.incoming));
_audioChannel->SetPlayout(true); _audioChannel->SetPlayout(true);
cricket::StreamParams videoSendStreamParams; _videoChannel->SetInterface(_videoNetworkInterface.get(), webrtc::MediaTransportConfig());
cricket::SsrcGroup videoSendSsrcGroup(cricket::kFecFrSsrcGroupSemantics, {_ssrcVideo.outgoing, _ssrcVideo.fecOutgoing});
videoSendStreamParams.ssrcs = {_ssrcVideo.outgoing};
videoSendStreamParams.ssrc_groups.push_back(videoSendSsrcGroup);
videoSendStreamParams.cname = "cname";
_videoChannel->AddSendStream(videoSendStreamParams);
auto videoCodec = selectVideoCodec(videoCodecs); _nativeVideoSource = makeVideoSource(_thread, getWorkerThread());
if (videoCodec.has_value()) {
_nativeVideoSource = makeVideoSource(_thread, getWorkerThread());
auto codec = videoCodec.value();
codec.SetParam(cricket::kCodecParamMinBitrate, 64);
codec.SetParam(cricket::kCodecParamStartBitrate, 512);
codec.SetParam(cricket::kCodecParamMaxBitrate, 2500);
_videoCapturer = makeVideoCapturer(_nativeVideoSource);
cricket::VideoSendParameters videoSendParameters;
videoSendParameters.codecs.push_back(codec);
for (auto &c : videoCodecs) {
if (c.name == cricket::kFlexfecCodecName) {
videoSendParameters.codecs.push_back(c);
break;
}
}
videoSendParameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, extensionSequenceOne);
//send_parameters.max_bandwidth_bps = 800000;
//send_parameters.rtcp.reduced_size = true;
//videoSendParameters.rtcp.remote_estimate = true;
_videoChannel->SetSendParameters(videoSendParameters);
_videoChannel->SetVideoSend(_ssrcVideo.outgoing, NULL, _nativeVideoSource.get());
_videoChannel->SetVideoSend(_ssrcVideo.fecOutgoing, NULL, nullptr);
_videoChannel->SetInterface(_videoNetworkInterface.get(), webrtc::MediaTransportConfig());
cricket::VideoRecvParameters videoRecvParameters;
videoRecvParameters.codecs.emplace_back(codec);
for (auto &c : videoCodecs) {
if (c.name == cricket::kFlexfecCodecName) {
videoRecvParameters.codecs.push_back(c);
break;
}
}
videoRecvParameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, extensionSequenceOne);
//recv_parameters.rtcp.reduced_size = true;
videoRecvParameters.rtcp.remote_estimate = true;
cricket::StreamParams videoRecvStreamParams;
cricket::SsrcGroup videoRecvSsrcGroup(cricket::kFecFrSsrcGroupSemantics, {_ssrcVideo.incoming, _ssrcVideo.fecIncoming});
videoRecvStreamParams.ssrcs = {_ssrcVideo.incoming};
videoRecvStreamParams.ssrc_groups.push_back(videoRecvSsrcGroup);
videoRecvStreamParams.cname = "cname";
_videoChannel->AddRecvStream(videoRecvStreamParams);
_videoChannel->SetRecvParameters(videoRecvParameters);
/*webrtc::FlexfecReceiveStream::Config config(_videoNetworkInterface.get());
config.payload_type = 118;
config.protected_media_ssrcs = {1324234};
webrtc::FlexfecReceiveStream* stream;
std::list<webrtc::FlexfecReceiveStream *> streams;*/
}
} }
MediaManager::~MediaManager() { MediaManager::~MediaManager() {
@ -334,18 +297,16 @@ MediaManager::~MediaManager() {
_audioChannel->SetInterface(nullptr, webrtc::MediaTransportConfig()); _audioChannel->SetInterface(nullptr, webrtc::MediaTransportConfig());
_videoChannel->RemoveRecvStream(_ssrcVideo.incoming); setSendVideo(false);
_videoChannel->RemoveRecvStream(_ssrcVideo.fecIncoming);
_videoChannel->RemoveSendStream(_ssrcVideo.outgoing);
_videoChannel->RemoveSendStream(_ssrcVideo.fecOutgoing);
_videoChannel->SetVideoSend(_ssrcVideo.outgoing, NULL, nullptr);
_videoChannel->SetVideoSend(_ssrcVideo.fecOutgoing, NULL, nullptr);
_videoChannel->SetInterface(nullptr, webrtc::MediaTransportConfig());
} }
void MediaManager::setIsConnected(bool isConnected) { void MediaManager::setIsConnected(bool isConnected) {
if (isConnected) { if (_isConnected == isConnected) {
return;
}
_isConnected = isConnected;
if (_isConnected) {
_call->SignalChannelNetworkState(webrtc::MediaType::AUDIO, webrtc::kNetworkUp); _call->SignalChannelNetworkState(webrtc::MediaType::AUDIO, webrtc::kNetworkUp);
_call->SignalChannelNetworkState(webrtc::MediaType::VIDEO, webrtc::kNetworkUp); _call->SignalChannelNetworkState(webrtc::MediaType::VIDEO, webrtc::kNetworkUp);
} else { } else {
@ -353,13 +314,13 @@ void MediaManager::setIsConnected(bool isConnected) {
_call->SignalChannelNetworkState(webrtc::MediaType::VIDEO, webrtc::kNetworkDown); _call->SignalChannelNetworkState(webrtc::MediaType::VIDEO, webrtc::kNetworkDown);
} }
if (_audioChannel) { if (_audioChannel) {
_audioChannel->OnReadyToSend(isConnected); _audioChannel->OnReadyToSend(_isConnected);
_audioChannel->SetSend(isConnected); _audioChannel->SetSend(_isConnected);
_audioChannel->SetAudioSend(_ssrcAudio.outgoing, isConnected, nullptr, &_audioSource); _audioChannel->SetAudioSend(_ssrcAudio.outgoing, _isConnected, nullptr, &_audioSource);
} }
if (_videoChannel) { if (_isSendingVideo && _videoChannel) {
_videoChannel->OnReadyToSend(isConnected); _videoChannel->OnReadyToSend(_isConnected);
_videoChannel->SetSend(isConnected); _videoChannel->SetSend(_isConnected);
} }
} }
@ -386,14 +347,113 @@ void MediaManager::notifyPacketSent(const rtc::SentPacket &sentPacket) {
_call->OnSentPacket(sentPacket); _call->OnSentPacket(sentPacket);
} }
void MediaManager::setSendVideo(bool sendVideo) {
if (_isSendingVideo == sendVideo) {
return;
}
_isSendingVideo = sendVideo;
if (_isSendingVideo) {
auto videoCodec = selectVideoCodec(_videoCodecs);
if (videoCodec.has_value()) {
auto codec = videoCodec.value();
codec.SetParam(cricket::kCodecParamMinBitrate, 64);
codec.SetParam(cricket::kCodecParamStartBitrate, 512);
codec.SetParam(cricket::kCodecParamMaxBitrate, 2500);
_videoCapturer = makeVideoCapturer(_nativeVideoSource, _useFrontCamera);
cricket::VideoSendParameters videoSendParameters;
videoSendParameters.codecs.push_back(codec);
for (auto &c : _videoCodecs) {
if (c.name == cricket::kFlexfecCodecName) {
videoSendParameters.codecs.push_back(c);
break;
}
}
videoSendParameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, 1);
//send_parameters.max_bandwidth_bps = 800000;
//send_parameters.rtcp.reduced_size = true;
//videoSendParameters.rtcp.remote_estimate = true;
_videoChannel->SetSendParameters(videoSendParameters);
cricket::StreamParams videoSendStreamParams;
cricket::SsrcGroup videoSendSsrcGroup(cricket::kFecFrSsrcGroupSemantics, {_ssrcVideo.outgoing, _ssrcVideo.fecOutgoing});
videoSendStreamParams.ssrcs = {_ssrcVideo.outgoing};
videoSendStreamParams.ssrc_groups.push_back(videoSendSsrcGroup);
videoSendStreamParams.cname = "cname";
_videoChannel->AddSendStream(videoSendStreamParams);
_videoChannel->SetVideoSend(_ssrcVideo.outgoing, NULL, _nativeVideoSource.get());
_videoChannel->SetVideoSend(_ssrcVideo.fecOutgoing, NULL, nullptr);
cricket::VideoRecvParameters videoRecvParameters;
for (auto &c : _videoCodecs) {
if (c.name == cricket::kFlexfecCodecName) {
videoRecvParameters.codecs.push_back(c);
} else if (c.name == cricket::kH264CodecName) {
videoRecvParameters.codecs.push_back(c);
} else if (c.name == cricket::kH265CodecName) {
videoRecvParameters.codecs.push_back(c);
} else if (c.name == cricket::kVp8CodecName) {
videoRecvParameters.codecs.push_back(c);
} else if (c.name == cricket::kVp9CodecName) {
videoRecvParameters.codecs.push_back(c);
} else if (c.name == cricket::kAv1CodecName) {
videoRecvParameters.codecs.push_back(c);
}
}
videoRecvParameters.extensions.emplace_back(webrtc::RtpExtension::kTransportSequenceNumberUri, 1);
//recv_parameters.rtcp.reduced_size = true;
videoRecvParameters.rtcp.remote_estimate = true;
cricket::StreamParams videoRecvStreamParams;
cricket::SsrcGroup videoRecvSsrcGroup(cricket::kFecFrSsrcGroupSemantics, {_ssrcVideo.incoming, _ssrcVideo.fecIncoming});
videoRecvStreamParams.ssrcs = {_ssrcVideo.incoming};
videoRecvStreamParams.ssrc_groups.push_back(videoRecvSsrcGroup);
videoRecvStreamParams.cname = "cname";
_videoChannel->AddRecvStream(videoRecvStreamParams);
_videoChannel->SetRecvParameters(videoRecvParameters);
if (_isSendingVideo && _videoChannel) {
_videoChannel->OnReadyToSend(_isConnected);
_videoChannel->SetSend(_isConnected);
}
}
} else {
_videoChannel->SetVideoSend(_ssrcVideo.outgoing, NULL, nullptr);
_videoChannel->SetVideoSend(_ssrcVideo.fecOutgoing, NULL, nullptr);
_videoCapturer.reset();
_videoChannel->RemoveRecvStream(_ssrcVideo.incoming);
_videoChannel->RemoveRecvStream(_ssrcVideo.fecIncoming);
_videoChannel->RemoveSendStream(_ssrcVideo.outgoing);
_videoChannel->RemoveSendStream(_ssrcVideo.fecOutgoing);
}
}
void MediaManager::switchVideoCamera() {
if (_isSendingVideo) {
_useFrontCamera = !_useFrontCamera;
_videoCapturer = makeVideoCapturer(_nativeVideoSource, _useFrontCamera);
}
}
void MediaManager::setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink) { void MediaManager::setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink) {
_currentIncomingVideoSink = sink; _currentIncomingVideoSink = sink;
_videoChannel->SetSink(_ssrcVideo.incoming, sink.get()); _videoChannel->SetSink(_ssrcVideo.incoming, _currentIncomingVideoSink.get());
} }
void MediaManager::setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink) { void MediaManager::setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink) {
_currentOutgoingVideoSink = sink; _currentOutgoingVideoSink = sink;
_nativeVideoSource->AddOrUpdateSink(sink.get(), rtc::VideoSinkWants()); _nativeVideoSource->AddOrUpdateSink(_currentOutgoingVideoSink.get(), rtc::VideoSinkWants());
} }
MediaManager::NetworkInterfaceImpl::NetworkInterfaceImpl(MediaManager *mediaManager, bool isVideo) : MediaManager::NetworkInterfaceImpl::NetworkInterfaceImpl(MediaManager *mediaManager, bool isVideo) :

View File

@ -64,6 +64,8 @@ public:
void setIsConnected(bool isConnected); void setIsConnected(bool isConnected);
void receivePacket(const rtc::CopyOnWriteBuffer &packet); void receivePacket(const rtc::CopyOnWriteBuffer &packet);
void notifyPacketSent(const rtc::SentPacket &sentPacket); void notifyPacketSent(const rtc::SentPacket &sentPacket);
void setSendVideo(bool sendVideo);
void switchVideoCamera();
void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink); void setIncomingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
void setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink); void setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
@ -78,6 +80,12 @@ private:
SSRC _ssrcAudio; SSRC _ssrcAudio;
SSRC _ssrcVideo; SSRC _ssrcVideo;
bool _isConnected;
std::vector<cricket::VideoCodec> _videoCodecs;
bool _isSendingVideo;
bool _useFrontCamera;
std::unique_ptr<cricket::MediaEngineInterface> _mediaEngine; std::unique_ptr<cricket::MediaEngineInterface> _mediaEngine;
std::unique_ptr<webrtc::Call> _call; std::unique_ptr<webrtc::Call> _call;
webrtc::FieldTrialBasedConfig _fieldTrials; webrtc::FieldTrialBasedConfig _fieldTrials;

View File

@ -175,18 +175,17 @@ _signalingDataEmitted(signalingDataEmitted) {
flags |= cricket::PORTALLOCATOR_DISABLE_UDP; flags |= cricket::PORTALLOCATOR_DISABLE_UDP;
flags |= cricket::PORTALLOCATOR_DISABLE_STUN; flags |= cricket::PORTALLOCATOR_DISABLE_STUN;
} }
//flags |= cricket::PORTALLOCATOR_DISABLE_UDP;
_portAllocator->set_flags(_portAllocator->flags() | flags); _portAllocator->set_flags(_portAllocator->flags() | flags);
_portAllocator->Initialize(); _portAllocator->Initialize();
rtc::SocketAddress defaultStunAddress = rtc::SocketAddress("hlgkfjdrtjfykgulhijkljhulyo.uksouth.cloudapp.azure.com", 3478); rtc::SocketAddress defaultStunAddress = rtc::SocketAddress("134.122.52.178", 3478);
cricket::ServerAddresses stunServers; cricket::ServerAddresses stunServers;
stunServers.insert(defaultStunAddress); stunServers.insert(defaultStunAddress);
std::vector<cricket::RelayServerConfig> turnServers; std::vector<cricket::RelayServerConfig> turnServers;
turnServers.push_back(cricket::RelayServerConfig( turnServers.push_back(cricket::RelayServerConfig(
rtc::SocketAddress("hlgkfjdrtjfykgulhijkljhulyo.uksouth.cloudapp.azure.com", 3478), rtc::SocketAddress("134.122.52.178", 3478),
"user", "openrelay",
"root", "openrelay",
cricket::PROTO_UDP cricket::PROTO_UDP
)); ));
_portAllocator->SetConfiguration(stunServers, turnServers, 2, webrtc::NO_PRUNE); _portAllocator->SetConfiguration(stunServers, turnServers, 2, webrtc::NO_PRUNE);
@ -233,7 +232,7 @@ NetworkManager::~NetworkManager() {
_socketFactory.reset(); _socketFactory.reset();
} }
void NetworkManager::receiveSignalingData(const std::vector<uint8_t> &data) { void NetworkManager::receiveSignalingData(const rtc::CopyOnWriteBuffer &data) {
rtc::ByteBufferReader reader((const char *)data.data(), data.size()); rtc::ByteBufferReader reader((const char *)data.data(), data.size());
uint32_t candidateCount = 0; uint32_t candidateCount = 0;
if (!reader.ReadUInt32(&candidateCount)) { if (!reader.ReadUInt32(&candidateCount)) {

View File

@ -47,7 +47,7 @@ public:
); );
~NetworkManager(); ~NetworkManager();
void receiveSignalingData(const std::vector<uint8_t> &data); void receiveSignalingData(const rtc::CopyOnWriteBuffer &data);
void sendPacket(const rtc::CopyOnWriteBuffer &packet); void sendPacket(const rtc::CopyOnWriteBuffer &packet);
private: private:

View File

@ -138,6 +138,7 @@ public:
TgVoipNetworkType initialNetworkType, TgVoipNetworkType initialNetworkType,
TgVoipEncryptionKey const &encryptionKey, TgVoipEncryptionKey const &encryptionKey,
std::function<void(TgVoipState)> stateUpdated, std::function<void(TgVoipState)> stateUpdated,
std::function<void(bool)> videoStateUpdated,
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
); );
@ -158,6 +159,8 @@ public:
virtual TgVoipPersistentState getPersistentState() = 0; virtual TgVoipPersistentState getPersistentState() = 0;
virtual void receiveSignalingData(const std::vector<uint8_t> &data) = 0; virtual void receiveSignalingData(const std::vector<uint8_t> &data) = 0;
virtual void setSendVideo(bool sendVideo) = 0;
virtual void switchVideoCamera() = 0;
virtual TgVoipFinalState stop() = 0; virtual TgVoipFinalState stop() = 0;
}; };

View File

@ -143,6 +143,7 @@ public:
TgVoipEncryptionKey const &encryptionKey, TgVoipEncryptionKey const &encryptionKey,
TgVoipNetworkType initialNetworkType, TgVoipNetworkType initialNetworkType,
std::function<void(TgVoipState)> stateUpdated, std::function<void(TgVoipState)> stateUpdated,
std::function<void(bool)> videoStateUpdated,
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
) : ) :
_stateUpdated(stateUpdated), _stateUpdated(stateUpdated),
@ -156,7 +157,7 @@ public:
bool enableP2P = config.enableP2P; bool enableP2P = config.enableP2P;
_manager.reset(new ThreadLocalObject<Manager>(getManagerThread(), [encryptionKey = encryptionKey, enableP2P = enableP2P, stateUpdated, signalingDataEmitted](){ _manager.reset(new ThreadLocalObject<Manager>(getManagerThread(), [encryptionKey = encryptionKey, enableP2P = enableP2P, stateUpdated, videoStateUpdated, signalingDataEmitted](){
return new Manager( return new Manager(
getManagerThread(), getManagerThread(),
encryptionKey, encryptionKey,
@ -164,6 +165,9 @@ public:
[stateUpdated](const TgVoipState &state) { [stateUpdated](const TgVoipState &state) {
stateUpdated(state); stateUpdated(state);
}, },
[videoStateUpdated](bool isActive) {
videoStateUpdated(isActive);
},
[signalingDataEmitted](const std::vector<uint8_t> &data) { [signalingDataEmitted](const std::vector<uint8_t> &data) {
signalingDataEmitted(data); signalingDataEmitted(data);
} }
@ -183,6 +187,18 @@ public:
manager->receiveSignalingData(data); manager->receiveSignalingData(data);
}); });
}; };
void setSendVideo(bool sendVideo) override {
_manager->perform([sendVideo](Manager *manager) {
manager->setSendVideo(sendVideo);
});
};
void switchVideoCamera() override {
_manager->perform([](Manager *manager) {
manager->switchVideoCamera();
});
}
void setNetworkType(TgVoipNetworkType networkType) override { void setNetworkType(TgVoipNetworkType networkType) override {
/*message::NetworkType mappedType; /*message::NetworkType mappedType;
@ -361,6 +377,7 @@ TgVoip *TgVoip::makeInstance(
TgVoipNetworkType initialNetworkType, TgVoipNetworkType initialNetworkType,
TgVoipEncryptionKey const &encryptionKey, TgVoipEncryptionKey const &encryptionKey,
std::function<void(TgVoipState)> stateUpdated, std::function<void(TgVoipState)> stateUpdated,
std::function<void(bool)> videoStateUpdated,
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
) { ) {
return new TgVoipImpl( return new TgVoipImpl(
@ -371,6 +388,7 @@ TgVoip *TgVoip::makeInstance(
encryptionKey, encryptionKey,
initialNetworkType, initialNetworkType,
stateUpdated, stateUpdated,
videoStateUpdated,
signalingDataEmitted signalingDataEmitted
); );
} }

View File

@ -23,6 +23,13 @@ typedef NS_ENUM(int32_t, OngoingCallStateWebrtc) {
OngoingCallStateReconnecting OngoingCallStateReconnecting
}; };
typedef NS_ENUM(int32_t, OngoingCallVideoStateWebrtc) {
OngoingCallVideoStateInactive,
OngoingCallVideoStateRequesting,
OngoingCallVideoStateInvited,
OngoingCallVideoStateActive
};
typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) { typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) {
OngoingCallNetworkTypeWifi, OngoingCallNetworkTypeWifi,
OngoingCallNetworkTypeCellularGprs, OngoingCallNetworkTypeCellularGprs,
@ -62,7 +69,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
+ (int32_t)maxLayer; + (int32_t)maxLayer;
+ (NSString * _Nonnull)version; + (NSString * _Nonnull)version;
@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallStateWebrtc); @property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallStateWebrtc, OngoingCallVideoStateWebrtc);
@property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t); @property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t);
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray<OngoingCallConnectionDescriptionWebrtc *> * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData; - (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray<OngoingCallConnectionDescriptionWebrtc *> * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData;
@ -75,6 +82,8 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
- (NSData * _Nonnull)getDerivedState; - (NSData * _Nonnull)getDerivedState;
- (void)setIsMuted:(bool)isMuted; - (void)setIsMuted:(bool)isMuted;
- (void)setVideoEnabled:(bool)videoEnabled;
- (void)switchVideoCamera;
- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType; - (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType;
- (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion; - (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;
- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion; - (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;

View File

@ -34,6 +34,8 @@ using namespace TGVOIP_NAMESPACE;
TgVoip *_tgVoip; TgVoip *_tgVoip;
OngoingCallStateWebrtc _state; OngoingCallStateWebrtc _state;
OngoingCallVideoStateWebrtc _videoState;
int32_t _signalBars; int32_t _signalBars;
NSData *_lastDerivedState; NSData *_lastDerivedState;
@ -127,6 +129,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
_callPacketTimeout = 10.0; _callPacketTimeout = 10.0;
_networkType = networkType; _networkType = networkType;
_sendSignalingData = [sendSignalingData copy]; _sendSignalingData = [sendSignalingData copy];
_videoState = OngoingCallVideoStateInactive;
std::vector<uint8_t> derivedStateValue; std::vector<uint8_t> derivedStateValue;
derivedStateValue.resize(derivedState.length); derivedStateValue.resize(derivedState.length);
@ -172,7 +175,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
.initializationTimeout = _callConnectTimeout, .initializationTimeout = _callConnectTimeout,
.receiveTimeout = _callPacketTimeout, .receiveTimeout = _callPacketTimeout,
.dataSaving = callControllerDataSavingForType(dataSaving), .dataSaving = callControllerDataSavingForType(dataSaving),
.enableP2P = allowP2P, .enableP2P = (bool)allowP2P,
.enableAEC = false, .enableAEC = false,
.enableNS = true, .enableNS = true,
.enableAGC = true, .enableAGC = true,
@ -206,6 +209,25 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
} }
}]; }];
}, },
[weakSelf, queue](bool isActive) {
[queue dispatch:^{
__strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf;
if (strongSelf) {
OngoingCallVideoStateWebrtc videoState;
if (isActive) {
videoState = OngoingCallVideoStateActive;
} else {
videoState = OngoingCallVideoStateInactive;
}
if (strongSelf->_videoState != videoState) {
strongSelf->_videoState = videoState;
if (strongSelf->_stateChanged) {
strongSelf->_stateChanged(strongSelf->_state, strongSelf->_videoState);
}
}
}
}];
},
[weakSelf, queue](const std::vector<uint8_t> &data) { [weakSelf, queue](const std::vector<uint8_t> &data) {
NSData *mappedData = [[NSData alloc] initWithBytes:data.data() length:data.size()]; NSData *mappedData = [[NSData alloc] initWithBytes:data.data() length:data.size()];
[queue dispatch:^{ [queue dispatch:^{
@ -300,7 +322,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
_state = callState; _state = callState;
if (_stateChanged) { if (_stateChanged) {
_stateChanged(callState); _stateChanged(_state, _videoState);
} }
} }
} }
@ -336,6 +358,18 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
} }
} }
- (void)setVideoEnabled:(bool)videoEnabled {
if (_tgVoip) {
_tgVoip->setSendVideo(videoEnabled);
}
}
- (void)switchVideoCamera {
if (_tgVoip) {
_tgVoip->switchVideoCamera();
}
}
- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType { - (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType {
if (_networkType != networkType) { if (_networkType != networkType) {
_networkType = networkType; _networkType = networkType;

View File

@ -15,16 +15,38 @@ genrule(
set -x set -x
echo "SRCDIR=$SRCDIR" echo "SRCDIR=$SRCDIR"
OUT_DIR="ios"
BUILD_ARCH=arm64 BUILD_ARCH=arm64
BUILD_DIR="$SRCDIR/$BUILD_ARCH" BUILD_DIR_ARM64="$SRCDIR/$BUILD_ARCH"
BUILD_DIR="$BUILD_DIR_ARM64"
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
mkdir -p "$BUILD_DIR/webrtc-ios"
cp -R "$SRCDIR/webrtc-ios/src" "$BUILD_DIR/webrtc-ios/src"
DEPOT_TOOLS_PATH="$(location //third-party:depot_tools_sources)"
rm -rf "$BUILD_DIR/depot_tools"
cp -R "$DEPOT_TOOLS_PATH" "$BUILD_DIR/"
rm -rf "$BUILD_DIR/openssl"
cp -R "$(location //submodules/openssl:openssl_build_merged)" "$BUILD_DIR/openssl/"
cp -R "$(location //submodules/openssl:openssl_libssl_merged)" "$BUILD_DIR/libssl/"
sh $SRCDIR/build-webrtc-buck.sh "$BUILD_DIR" $BUILD_ARCH
BUILD_ARCH=arm
BUILD_DIR_ARMV7="$SRCDIR/$BUILD_ARCH"
BUILD_DIR="$BUILD_DIR_ARMV7"
rm -rf "$BUILD_DIR" rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR" mkdir -p "$BUILD_DIR"
mkdir -p "$BUILD_DIR/webrtc-ios" mkdir -p "$BUILD_DIR/webrtc-ios"
cp -R "$SRCDIR/webrtc-ios/.git" "$BUILD_DIR/webrtc-ios/.git"
cp -R "$SRCDIR/webrtc-ios/src" "$BUILD_DIR/webrtc-ios/src" cp -R "$SRCDIR/webrtc-ios/src" "$BUILD_DIR/webrtc-ios/src"
DEPOT_TOOLS_PATH="$(location //third-party:depot_tools_sources)" DEPOT_TOOLS_PATH="$(location //third-party:depot_tools_sources)"
@ -39,7 +61,7 @@ genrule(
sh $SRCDIR/build-webrtc-buck.sh "$BUILD_DIR" $BUILD_ARCH sh $SRCDIR/build-webrtc-buck.sh "$BUILD_DIR" $BUILD_ARCH
mkdir -p "$OUT" mkdir -p "$OUT"
cp "$BUILD_DIR/webrtc-ios/src/out/$OUT_DIR/obj/sdk/libframework_objc_static.a" "$OUT/" lipo -create "$BUILD_DIR_ARMV7/webrtc-ios/src/out/ios/obj/sdk/libframework_objc_static.a" "$BUILD_DIR_ARM64/webrtc-ios/src/out/ios_64/obj/sdk/libframework_objc_static.a" -output "$OUT/libframework_objc_static.a"
""", """,
out = "libwebrtc", out = "libwebrtc",
visibility = ["PUBLIC"] visibility = ["PUBLIC"]

View File

@ -35,7 +35,7 @@ genrule(
echo "Unsupported architecture $(TARGET_CPU)" echo "Unsupported architecture $(TARGET_CPU)"
fi fi
BUILD_DIR="$(RULEDIR)/$$BUILD_ARCH" BUILD_DIR="$(RULEDIR)/$$BUILD_ARCH"
rm -rf "$$BUILD_DIR" #rm -rf "$$BUILD_DIR"
mkdir -p "$$BUILD_DIR" mkdir -p "$$BUILD_DIR"
SOURCE_PATH="third-party/webrtc/webrtc-ios/src" SOURCE_PATH="third-party/webrtc/webrtc-ios/src"

View File

@ -21,7 +21,9 @@ mv openssl/lib/libcrypto.a openssl/
mv libssl/lib/libssl.a openssl/ mv libssl/lib/libssl.a openssl/
OUT_DIR="ios" OUT_DIR="ios"
if [ "$ARCH" == "x64" ]; then if [ "$ARCH" == "arm64" ]; then
OUT_DIR="ios_64"
elif [ "$ARCH" == "x64" ]; then
OUT_DIR="ios_sim" OUT_DIR="ios_sim"
fi fi