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
APP_VERSION="6.2.1"
APP_VERSION="6.3"
CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
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.GroupActionsTitle" = "ACTIONS";
"Stats.GroupTopHoursTitle" = "TOP HOURS";
"Stats.GroupTopWeekdaysTitle" = "TOP DAYS OF WEEK";
"Stats.GroupTopPostersTitle" = "TOP MEMBERS";
"Stats.GroupTopAdminsTitle" = "TOP ADMINS";
"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_any" = "%@ symbols per message";
"Stats.GroupTopPoster.History" = "History";
"Stats.GroupTopPoster.Promote" = "Promote";
"Stats.GroupTopAdminDeletions_0" = "%@ deletions";
"Stats.GroupTopAdminDeletions_1" = "%@ deletion";
"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_any" = "%@ bans";
"Stats.GroupTopAdmin.Actions" = "Actions";
"Stats.GroupTopAdmin.Promote" = "Promote";
"Stats.GroupTopInviterInvites_0" = "%@ invitations";
"Stats.GroupTopInviterInvites_1" = "%@ invitation";
"Stats.GroupTopInviterInvites_2" = "%@ invitations";
"Stats.GroupTopInviterInvites_3_10" = "%@ invitations";
"Stats.GroupTopInviterInvites_many" = "%@ 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 let navigationController: NavigationController
public let chatController: ChatController?
@ -227,7 +255,7 @@ public final class NavigateToChatControllerParams {
public let useExisting: Bool
public let purposefulAction: (() -> Void)?
public let scrollToEndIfExists: Bool
public let activateMessageSearch: Bool
public let activateMessageSearch: (ChatSearchDomain, String)?
public let peekData: ChatPeekTimeout?
public let peerNearbyData: ChatPeerNearbyData?
public let animated: Bool
@ -235,7 +263,7 @@ public final class NavigateToChatControllerParams {
public let parentGroupId: PeerGroupId?
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.chatController = chatController
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 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 makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController?
func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController
func makePeersNearbyController(context: AccountContext) -> ViewController
func makeComposeController(context: AccountContext) -> ViewController

View File

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

View File

@ -666,6 +666,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
case .destructive:
color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor
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))
index += 1

View File

@ -30,6 +30,12 @@ public class PercentPieChartController: BaseChartController {
let pieController: PieChartComponentController
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) {
transitionRenderer = PercentPieAnimationRenderer()
percentController = PercentChartComponentController(isZoomed: false,
@ -88,14 +94,14 @@ public class PercentPieChartController: BaseChartController {
pieController.previewBarChartRenderer]
}
public override func initializeChart() {
public override func initializeChart() {
percentController.initialize(chartsCollection: initialChartsCollection,
initialDate: Date(),
totalHorizontalRange: BaseConstants.defaultRange,
totalVerticalRange: BaseConstants.defaultRange)
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
self.didTapZoomIn(date: lastDate, animated: false)
TimeInterval.animationDurationMultipler = 1.0

View File

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

View File

@ -274,6 +274,7 @@ public enum ItemListPeerItemRevealOptionType {
case neutral
case warning
case destructive
case accent
}
public struct ItemListPeerItemRevealOption {
@ -624,6 +625,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
case .destructive:
color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor
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))
index += 1

View File

@ -21,7 +21,7 @@
@property (nonatomic, copy) void (^micLevel)(CGFloat level);
@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(^onDismiss)(bool isAuto);
@property (nonatomic, copy) void(^onDismiss)(bool isAuto, bool isCancelled);
@property (nonatomic, copy) void(^onStop)(void);
@property (nonatomic, copy) void(^onCancel)(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.alpha = 0.0f;
_lock.transform = CGAffineTransformIdentity;
if (iosMajorVersion() >= 8) {
[UIView animateWithDuration:0.50 delay:0.0 usingSpringWithDamping:0.55f initialSpringVelocity:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^{
_innerCircleView.transform = CGAffineTransformIdentity;
@ -548,6 +550,10 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
}
- (void)animateLock {
if (!_animatedIn) {
return;
}
_lockView.lockness = 1.0f;
[_lock updateLockness:1.0];
@ -718,10 +724,6 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
return false;
} else if (distanceX < -100.0 && !_xFeedbackOccured) {
if (iosMajorVersion() >= 10) {
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[generator impactOccurred];
}
_xFeedbackOccured = true;
} else if (distanceX > -100.0) {
_xFeedbackOccured = false;
@ -732,10 +734,6 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
return false;
} else if (distanceY < -60.0 && !_yFeedbackOccured) {
if (iosMajorVersion() >= 10) {
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[generator impactOccurred];
}
_yFeedbackOccured = true;
} else if (distanceY > -60.0) {
_yFeedbackOccured = false;

View File

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

View File

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

View File

@ -43,7 +43,7 @@ private enum StatsSection: Int32 {
}
private enum StatsEntry: ItemListNodeEntry {
case overviewHeader(PresentationTheme, String, String)
case overviewTitle(PresentationTheme, String, String)
case overview(PresentationTheme, ChannelStats)
case growthTitle(PresentationTheme, String)
@ -78,7 +78,7 @@ private enum StatsEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .overviewHeader, .overview:
case .overviewTitle, .overview:
return StatsSection.overview.rawValue
case .growthTitle, .growthGraph:
return StatsSection.growth.rawValue
@ -105,7 +105,7 @@ private enum StatsEntry: ItemListNodeEntry {
var stableId: Int32 {
switch self {
case .overviewHeader:
case .overviewTitle:
return 0
case .overview:
return 1
@ -154,8 +154,8 @@ private enum StatsEntry: ItemListNodeEntry {
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
switch lhs {
case let .overviewHeader(lhsTheme, lhsText, lhsDates):
if case let .overviewHeader(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
case let .overviewTitle(lhsTheme, lhsText, lhsDates):
if case let .overviewTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true
} else {
return false
@ -296,7 +296,7 @@ private enum StatsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelStatsControllerArguments
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)
case let .growthTitle(_, 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 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))
if !data.growthGraph.isEmpty {

View File

@ -21,11 +21,23 @@ private final class GroupStatsControllerArguments {
let context: AccountContext
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
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.loadDetailedGraph = loadDetailedGraph
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 actions
case topHours
case topWeekdays
case topPosters
case topAdmins
case topInviters
}
private enum StatsEntry: ItemListNodeEntry {
case overviewHeader(PresentationTheme, String, String)
case overviewTitle(PresentationTheme, String, String)
case overview(PresentationTheme, GroupStats)
case growthTitle(PresentationTheme, String)
@ -68,18 +81,21 @@ private enum StatsEntry: ItemListNodeEntry {
case topHoursTitle(PresentationTheme, String)
case topHoursGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case topPostersTitle(PresentationTheme, String)
case topPoster(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopPoster)
case topWeekdaysTitle(PresentationTheme, String)
case topWeekdaysGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case topAdminsTitle(PresentationTheme, String)
case topAdmin(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopAdmin)
case topPostersTitle(PresentationTheme, String, String)
case topPoster(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopPoster, Bool)
case topInvitersTitle(PresentationTheme, String)
case topInviter(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, GroupStatsTopInviter)
case topAdminsTitle(PresentationTheme, String, String)
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 {
switch self {
case .overviewHeader, .overview:
case .overviewTitle, .overview:
return StatsSection.overview.rawValue
case .growthTitle, .growthGraph:
return StatsSection.growth.rawValue
@ -95,6 +111,8 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.actions.rawValue
case .topHoursTitle, .topHoursGraph:
return StatsSection.topHours.rawValue
case .topWeekdaysTitle, .topWeekdaysGraph:
return StatsSection.topWeekdays.rawValue
case .topPostersTitle, .topPoster:
return StatsSection.topPosters.rawValue
case .topAdminsTitle, .topAdmin:
@ -106,7 +124,7 @@ private enum StatsEntry: ItemListNodeEntry {
var stableId: Int32 {
switch self {
case .overviewHeader:
case .overviewTitle:
return 0
case .overview:
return 1
@ -138,25 +156,29 @@ private enum StatsEntry: ItemListNodeEntry {
return 14
case .topHoursGraph:
return 15
case .topWeekdaysTitle:
return 16
case .topWeekdaysGraph:
return 17
case .topPostersTitle:
return 1000
case let .topPoster(index, _, _, _, _, _):
case let .topPoster(index, _, _, _, _, _, _):
return 1001 + index
case .topAdminsTitle:
return 2000
case let .topAdmin(index, _, _, _, _, _):
case let .topAdmin(index, _, _, _, _, _, _):
return 2001 + index
case .topInvitersTitle:
return 3000
case let .topInviter(index, _, _, _, _, _):
case let .topInviter(index, _, _, _, _, _, _):
return 30001 + index
}
}
static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool {
switch lhs {
case let .overviewHeader(lhsTheme, lhsText, lhsDates):
if case let .overviewHeader(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
case let .overviewTitle(lhsTheme, lhsText, lhsDates):
if case let .overviewTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true
} else {
return false
@ -251,38 +273,50 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .topPostersTitle(lhsTheme, lhsText):
if case let .topPostersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .topWeekdaysTitle(lhsTheme, lhsText):
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
} else {
return false
}
case let .topPoster(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopPoster):
if case let .topPoster(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopPoster) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopPoster == rhsTopPoster {
case let .topPostersTitle(lhsTheme, lhsText, lhsDates):
if case let .topPostersTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true
} else {
return false
}
case let .topAdminsTitle(lhsTheme, lhsText):
if case let .topAdminsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .topPoster(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopPoster, lhsRevealed):
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
} else {
return false
}
case let .topAdmin(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopAdmin):
if case let .topAdmin(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopAdmin) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopAdmin == rhsTopAdmin {
case let .topAdminsTitle(lhsTheme, lhsText, lhsDates):
if case let .topAdminsTitle(rhsTheme, rhsText, rhsDates) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsDates == rhsDates {
return true
} else {
return false
}
case let .topInvitersTitle(lhsTheme, lhsText):
if case let .topInvitersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .topAdmin(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopAdmin, lhsRevealed):
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
} else {
return false
}
case let .topInviter(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsTopInviter):
if case let .topInviter(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsTopInviter) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsTopInviter == rhsTopInviter {
case let .topInvitersTitle(lhsTheme, lhsText, lhsDates):
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
} else {
return false
@ -297,7 +331,7 @@ private enum StatsEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GroupStatsControllerArguments
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)
case let .growthTitle(_, text),
let .membersTitle(_, text),
@ -306,10 +340,12 @@ private enum StatsEntry: ItemListNodeEntry {
let .messagesTitle(_, text),
let .actionsTitle(_, text),
let .topHoursTitle(_, text),
let .topPostersTitle(_, text),
let .topAdminsTitle(_, text),
let .topInvitersTitle(_, text):
let .topWeekdaysTitle(_, text):
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):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
case let .growthGraph(_, _, _, graph, type),
@ -318,9 +354,10 @@ private enum StatsEntry: ItemListNodeEntry {
let .languagesGraph(_, _, _, graph, type),
let .messagesGraph(_, _, _, 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)
case let .topPoster(_, _, strings, dateTimeFormat, peer, topPoster):
case let .topPoster(_, _, strings, dateTimeFormat, peer, topPoster, revealed):
var textComponents: [String] = []
if topPoster.messageCount > 0 {
textComponents.append(strings.Stats_GroupTopPosterMessages(topPoster.messageCount))
@ -328,10 +365,19 @@ private enum StatsEntry: ItemListNodeEntry {
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)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .topAdmin(_, _, strings, dateTimeFormat, peer, topAdmin):
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
arguments.setPostersPeerIdWithRevealedOptions(peerId, fromPeerId)
}, removePeer: { _ in })
case let .topAdmin(_, _, strings, dateTimeFormat, peer, topAdmin, revealed):
var textComponents: [String] = []
if topAdmin.deletedCount > 0 {
textComponents.append(strings.Stats_GroupTopAdminDeletions(topAdmin.deletedCount))
@ -342,29 +388,46 @@ private enum StatsEntry: ItemListNodeEntry {
if topAdmin.bannedCount > 0 {
textComponents.append(strings.Stats_GroupTopAdminBans(topAdmin.bannedCount))
}
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_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)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .topInviter(_, _, strings, dateTimeFormat, peer, topInviter):
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
arguments.setAdminsPeerIdWithRevealedOptions(peerId, fromPeerId)
}, removePeer: { _ in })
case let .topInviter(_, _, strings, dateTimeFormat, peer, topInviter, revealed):
var textComponents: [String] = []
textComponents.append(strings.Stats_GroupTopInviterInvites(topInviter.inviteCount))
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)
}, 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] = []
if let data = data {
let minDate = stringForDate(timestamp: data.period.minDate, 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))
if !data.growthGraph.isEmpty {
@ -399,36 +462,41 @@ private func groupStatsControllerEntries(data: GroupStats?, peers: [PeerId: Peer
if !data.topHoursGraph.isEmpty {
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 !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
for topPoster in data.topPosters {
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
}
}
}
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
for topAdmin in data.topAdmins {
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
}
}
}
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
for topInviter in data.topInviters {
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
}
}
@ -439,8 +507,55 @@ private func groupStatsControllerEntries(data: GroupStats?, peers: [PeerId: Peer
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 {
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 dataPromise = Promise<GroupStats?>(nil)
@ -450,6 +565,14 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
if let cachedData = cachedPeerData as? CachedChannelData {
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 dataSignal: Signal<GroupStats?, NoError> = statsContext.state
@ -457,7 +580,7 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
return state.stats
} |> afterNext({ [weak statsContext] stats in
if let statsContext = statsContext, let stats = stats {
if case .OnDemand = stats.newMembersBySourceGraph {
if case .OnDemand = stats.topWeekdaysGraph {
statsContext.loadGrowthGraph()
statsContext.loadMembersGraph()
statsContext.loadNewMembersBySourceGraph()
@ -465,6 +588,7 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
statsContext.loadMessagesGraph()
statsContext.loadActionsGraph()
statsContext.loadTopHoursGraph()
statsContext.loadTopWeekdaysGraph()
}
}
})
@ -503,15 +627,45 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached
return statsContext.loadDetailedGraph(graph, x: x)
}, openPeer: { peerId in
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 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
|> map { presentationData, data, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { state, presentationData, data, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data)
var emptyStateItem: ItemListControllerEmptyStateItem?
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 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))
}
@ -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
}

View File

@ -544,7 +544,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1918567619] = { return Api.Updates.parse_updatesCombined($0) }
dict[1957577280] = { return Api.Updates.parse_updates($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[1038967584] = { return Api.MessageMedia.parse_messageMediaEmpty($0) }
dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) }

View File

@ -663,13 +663,13 @@ public struct stats {
}
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) {
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 {
buffer.appendInt32(447818040)
buffer.appendInt32(-276825834)
}
period.serialize(buffer, true)
members.serialize(buffer, true)
@ -683,6 +683,7 @@ public struct stats {
messagesGraph.serialize(buffer, true)
actionsGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
weekdaysGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topPosters.count))
for item in topPosters {
@ -709,8 +710,8 @@ public struct stats {
public func descriptionFields() -> (String, [(String, Any)]) {
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):
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)])
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), ("weekdaysGraph", weekdaysGraph), ("topPosters", topPosters), ("topAdmins", topAdmins), ("topInviters", topInviters), ("users", users)])
}
}
@ -763,21 +764,25 @@ public struct stats {
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: [Api.StatsGroupTopPoster]?
if let _ = reader.readInt32() {
_13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self)
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: [Api.StatsGroupTopAdmin]?
var _14: [Api.StatsGroupTopPoster]?
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() {
_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() {
_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 _c2 = _2 != nil
@ -795,8 +800,9 @@ public struct stats {
let _c14 = _14 != nil
let _c15 = _15 != nil
let _c16 = _16 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 {
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!)
let _c17 = _17 != nil
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 {
return nil

View File

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

View File

@ -11,6 +11,7 @@ enum CallControllerButtonType {
case accept
case speaker
case bluetooth
case video
}
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)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
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
@ -209,6 +215,11 @@ final class CallControllerButtonNode: HighlightTrackingButtonNode {
regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear)
highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill)
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

View File

@ -15,7 +15,13 @@ enum CallControllerButtonsSpeakerMode {
}
enum CallControllerButtonsMode: Equatable {
case active(CallControllerButtonsSpeakerMode)
enum VideoState: Equatable {
case notAvailable
case available(Bool)
case active
}
case active(speakerMode: CallControllerButtonsSpeakerMode, videoState: VideoState)
case incoming
}
@ -27,6 +33,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
private let endButton: CallControllerButtonNode
private let speakerButton: CallControllerButtonNode
private let videoButton: CallControllerButtonNode
private var mode: CallControllerButtonsMode?
private var validLayout: CGFloat?
@ -41,6 +49,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
var mute: (() -> Void)?
var end: (() -> Void)?
var speaker: (() -> Void)?
var toggleVideo: (() -> Void)?
init(strings: PresentationStrings) {
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.alpha = 0.0
self.videoButton = CallControllerButtonNode(type: .video, label: nil)
self.videoButton.alpha = 0.0
super.init()
self.addSubnode(self.acceptButton)
@ -62,12 +74,14 @@ final class CallControllerButtonsNode: ASDisplayNode {
self.addSubnode(self.muteButton)
self.addSubnode(self.endButton)
self.addSubnode(self.speakerButton)
self.addSubnode(self.videoButton)
self.acceptButton.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.endButton.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) {
@ -107,6 +121,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0)
for button in [self.muteButton, self.endButton, self.speakerButton] {
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
}
@ -121,10 +140,10 @@ final class CallControllerButtonsNode: ASDisplayNode {
for button in [self.declineButton, self.acceptButton] {
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
}
case let .active(speakerMode):
case let .active(speakerMode, videoState):
for button in [self.muteButton, self.speakerButton] {
if animated && button.alpha.isZero {
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
}
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 animated {
self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
@ -187,6 +223,26 @@ final class CallControllerButtonsNode: ASDisplayNode {
self.speaker?()
} else if button === self.acceptButton {
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 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 {
private let sharedContext: SharedAccountContext
private let account: Account
@ -31,8 +74,8 @@ final class CallControllerNode: ASDisplayNode {
private let imageNode: TransformImageNode
private let dimNode: ASDisplayNode
private var incomingVideoView: UIView?
private var outgoingVideoView: UIView?
private var incomingVideoNode: IncomingVideoNode?
private var outgoingVideoNode: OutgoingVideoNode?
private var videoViewsRequested: Bool = false
private let backButtonArrowNode: ASImageNode
private let backButtonNode: HighlightableButtonNode
@ -63,6 +106,7 @@ final class CallControllerNode: ASDisplayNode {
var beginAudioOuputSelection: (() -> Void)?
var acceptCall: (() -> Void)?
var endCall: (() -> Void)?
var toggleVideo: (() -> Void)?
var back: (() -> Void)?
var presentCallRating: ((CallId) -> Void)?
var callEnded: ((Bool) -> Void)?
@ -151,6 +195,10 @@ final class CallControllerNode: ASDisplayNode {
self?.acceptCall?()
}
self.buttonsNode.toggleVideo = { [weak self] in
self?.toggleVideo?()
}
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
@ -205,7 +253,58 @@ final class CallControllerNode: ASDisplayNode {
let statusValue: CallControllerStatusValue
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:
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
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):
let strings = self.presentationData.strings
var isReconnecting = false
if case .reconnecting = callState {
if case .reconnecting = callState.state {
isReconnecting = true
}
statusValue = .timer({ value in
@ -266,45 +365,8 @@ final class CallControllerNode: ASDisplayNode {
}
}
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:
if !self.statusNode.alpha.isEqual(to: 0.5) {
self.statusNode.alpha = 0.5
@ -327,7 +389,7 @@ final class CallControllerNode: ASDisplayNode {
}
}
if self.shouldStayHiddenUntilConnection {
switch callState {
switch callState.state {
case .connecting, .active:
self.containerNode.alpha = 1.0
default:
@ -339,7 +401,7 @@ final class CallControllerNode: ASDisplayNode {
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
if presentRating {
self.presentCallRating?(callId)
@ -353,7 +415,7 @@ final class CallControllerNode: ASDisplayNode {
return
}
switch callState {
switch callState.state {
case .ringing:
self.buttonsNode.updateMode(.incoming)
default:
@ -373,7 +435,16 @@ final class CallControllerNode: ASDisplayNode {
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
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 {
incomingVideoView.frame = CGRect(origin: CGPoint(), size: layout.size)
if let incomingVideoNode = self.incomingVideoNode {
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))
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

View File

@ -188,7 +188,7 @@ public final class PresentationCallImpl: PresentationCall {
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> {
return self.statePromise.get()
}
@ -402,7 +402,7 @@ public final class PresentationCallImpl: PresentationCall {
switch sessionState.state {
case .ringing:
presentationState = .ringing
presentationState = PresentationCallState(state: .ringing, videoState: .notAvailable)
if previous == nil || previousControl == nil {
if !self.reportedIncomingCall {
self.reportedIncomingCall = true
@ -429,19 +429,28 @@ public final class PresentationCallImpl: PresentationCall {
}
case .accepting:
self.callWasActive = true
presentationState = .connecting(nil)
presentationState = PresentationCallState(state: .connecting(nil), videoState: .notAvailable)
case .dropping:
presentationState = .terminating
presentationState = PresentationCallState(state: .terminating, videoState: .notAvailable)
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):
presentationState = .requesting(ringing)
presentationState = PresentationCallState(state: .requesting(ringing), videoState: .notAvailable)
case let .active(_, _, keyVisualHash, _, _, _, _):
self.callWasActive = true
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:
presentationState = .connecting(keyVisualHash)
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState)
case .failed:
presentationState = nil
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
@ -453,7 +462,7 @@ public final class PresentationCallImpl: PresentationCall {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
presentationState = .active(timestamp, reception, keyVisualHash)
presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState)
case .reconnecting:
let timestamp: Double
if let activeTimestamp = self.activeTimestamp {
@ -462,10 +471,10 @@ public final class PresentationCallImpl: PresentationCall {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
presentationState = .reconnecting(timestamp, reception, keyVisualHash)
presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState)
}
} 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?) {
var tone: PresentationCallTone?
if let callContextState = callContextState, case .reconnecting = callContextState {
if let callContextState = callContextState, case .reconnecting = callContextState.state {
tone = .connecting
} else if let previous = previous {
switch previous.state {
case .accepting, .active, .dropping, .requesting:
switch state {
switch state.state {
case .connecting:
if case .requesting = previous.state {
tone = .ringing
@ -652,6 +661,14 @@ public final class PresentationCallImpl: PresentationCall {
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) {
guard self.currentAudioOutputValue != output else {
return

View File

@ -553,11 +553,12 @@ public final class GroupStats: Equatable {
public let messagesGraph: StatsGraph
public let actionsGraph: StatsGraph
public let topHoursGraph: StatsGraph
public let topWeekdaysGraph: StatsGraph
public let topPosters: [GroupStatsTopPoster]
public let topAdmins: [GroupStatsTopAdmin]
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.members = members
self.messages = messages
@ -570,6 +571,7 @@ public final class GroupStats: Equatable {
self.messagesGraph = messagesGraph
self.actionsGraph = actionsGraph
self.topHoursGraph = topHoursGraph
self.topWeekdaysGraph = topWeekdaysGraph
self.topPosters = topPosters
self.topAdmins = topAdmins
self.topInviters = topInviters
@ -612,6 +614,9 @@ public final class GroupStats: Equatable {
if lhs.topHoursGraph != rhs.topHoursGraph {
return false
}
if lhs.topWeekdaysGraph != rhs.topWeekdaysGraph {
return false
}
if lhs.topPosters != rhs.topPosters {
return false
}
@ -625,31 +630,35 @@ public final class GroupStats: Equatable {
}
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 {
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 {
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 {
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 {
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 {
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 {
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
|> mapToSignal { result -> Signal<GroupStats?, MTRpcError> in
return postbox.transaction { transaction -> GroupStats? in
if case let .megagroupStats(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, users) = result {
if case let .megagroupStats(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, users) = result {
var parsedUsers: [Peer] = []
for user in users {
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> {
if let token = graph.token {
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> {
return Signal { subscriber in
let disposable = MetaDisposable()
@ -973,7 +1003,11 @@ extension StatsGraph {
case let .statsGraphError(error):
self = .Failed(error: error)
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 {
convenience init(apiMegagroupStats: Api.stats.MegagroupStats) {
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 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 focusOnSearchAfterAppearance: Bool = false
private var focusOnSearchAfterAppearance: (ChatSearchDomain, String)?
private let keepPeerInfoScreenDataHotDisposable = MetaDisposable()
@ -2550,7 +2550,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
$0.updatedInputTextPanelState { panelState in
if let videoRecorder = videoRecorder {
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 {
return panelState.withUpdatedMediaRecordingState(nil)
@ -2562,12 +2562,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let videoRecorder = videoRecorder {
strongSelf.recorderFeedback?.impact(.light)
videoRecorder.onDismiss = {
if let strongSelf = self {
strongSelf.beginMediaRecordingRequestId += 1
strongSelf.lockMediaRecordingRequestId = nil
strongSelf.videoRecorder.set(.single(nil))
}
videoRecorder.onDismiss = { [weak self] isCancelled in
self?.chatDisplayNode.updateRecordedMediaDeleted(isCancelled)
self?.beginMediaRecordingRequestId += 1
self?.lockMediaRecordingRequestId = nil
self?.videoRecorder.set(.single(nil))
}
videoRecorder.onStop = {
if let strongSelf = self {
@ -4738,9 +4737,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.interfaceInteraction = interfaceInteraction
if self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = false
self.interfaceInteraction?.beginMessageSearch(.everything, "")
if let search = self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = nil
self.interfaceInteraction?.beginMessageSearch(search.0, search.1)
}
self.chatDisplayNode.interfaceInteraction = interfaceInteraction
@ -5005,8 +5004,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
if self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = false
if let _ = self.focusOnSearchAfterAppearance {
self.focusOnSearchAfterAppearance = nil
if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode {
searchNode.activate()
}
@ -9253,9 +9252,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
func activateSearch() {
self.focusOnSearchAfterAppearance = true
self.interfaceInteraction?.beginMessageSearch(.everything, "")
func activateSearch(domain: ChatSearchDomain = .everything, query: String = "") {
self.focusOnSearchAfterAppearance = (domain, query)
self.interfaceInteraction?.beginMessageSearch(domain, query)
}
}

View File

@ -128,35 +128,6 @@ struct ChatSearchResultsState: Equatable {
let totalCount: Int32
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 {
case none

View File

@ -1011,9 +1011,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if !deltaOffset.isZero {
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")
slideJuggleAnimation.toValue = CATransform3DMakeTranslation(-6, 0, 0)
slideJuggleAnimation.toValue = CATransform3DMakeTranslation(6, 0, 0)
slideJuggleAnimation.duration = 1
slideJuggleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
slideJuggleAnimation.autoreverses = true
@ -1023,11 +1023,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0))
let cancelMinX = audioRecordingCancelIndicator.alpha > 0.5 ? audioRecordingCancelIndicator.frame.minX : width
audioRecordingInfoContainerNode.frame = CGRect(
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
),
size: CGSize(width: baseWidth, height: panelHeight)
@ -1051,7 +1049,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
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))
if animateDotAppearing {
@ -1074,9 +1072,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
if hideInfo {
audioRecordingDotNode.layer.animateAlpha(from: audioRecordingDotNode.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingTimeNode.layer.animateAlpha(from: audioRecordingTimeNode.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingCancelIndicator.layer.animateAlpha(from: audioRecordingCancelIndicator.alpha, to: 0, duration: 0.15, delay: 0, removeOnCompletion: false)
audioRecordingDotNode.layer.removeAllAnimations()
audioRecordingDotNode.layer.animateAlpha(from: CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1), 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 {
self.actionButtons.micButton.audioRecorder = nil
@ -1091,8 +1090,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if let audioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
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
audioRecordingInfoContainerNode?.removeFromSupernode()
}
@ -1104,7 +1101,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
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?.removeFromSupernode()
}

View File

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

View File

@ -34,8 +34,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
controller.scrollToEndOfHistory()
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller)
} else if params.activateMessageSearch {
controller.activateSearch()
} else if let search = params.activateMessageSearch {
controller.activateSearch(domain: search.0, query: search.1)
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
params.completion(controller)
} 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.purposefulAction = params.purposefulAction
if params.activateMessageSearch {
controller.activateSearch()
if let search = params.activateMessageSearch {
controller.activateSearch(domain: search.0, query: search.1)
}
let resolvedKeepStack: Bool
switch params.keepStack {

View File

@ -2410,7 +2410,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
private func openChatWithMessageSearch() {
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 {
let resolvedText: CallStatusText
if let state = state {
switch state {
switch state.state {
case .connecting, .requesting, .terminating, .ringing, .waiting:
resolvedText = .inProgress(nil)
case .terminated:
@ -1014,6 +1014,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
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) {
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 {
return nil
}
if case .ringing = state {
if case .ringing = state.state {
return (peer, internalId)
} else {
return nil

View File

@ -93,11 +93,20 @@ private let setupLogs: Bool = {
return true
}()
public enum OngoingCallContextState {
case initializing
case connected
case reconnecting
case failed
public struct OngoingCallContextState: Equatable {
public enum State {
case initializing
case connected
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*/ {
@ -226,6 +235,8 @@ private func ongoingDataSavingForTypeWebrtc(_ type: VoiceCallDataSaving) -> Ongo
private protocol OngoingCallThreadLocalContextProtocol: class {
func nativeSetNetworkType(_ type: NetworkType)
func nativeSetIsMuted(_ value: Bool)
func nativeSetVideoEnabled(_ value: Bool)
func nativeSwitchVideoCamera()
func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void)
func nativeDebugInfo() -> String
func nativeVersion() -> String
@ -253,6 +264,12 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol {
self.setIsMuted(value)
}
func nativeSetVideoEnabled(_ value: Bool) {
}
func nativeSwitchVideoCamera() {
}
func nativeDebugInfo() -> String {
return self.debugInfo() ?? ""
}
@ -279,6 +296,14 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt
self.setIsMuted(value)
}
func nativeSetVideoEnabled(_ value: Bool) {
self.setVideoEnabled(value)
}
func nativeSwitchVideoCamera() {
self.switchVideoCamera()
}
func nativeDebugInfo() -> String {
return self.debugInfo() ?? ""
}
@ -318,7 +343,7 @@ extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProt
}
}*/
private extension OngoingCallContextState {
private extension OngoingCallContextState.State {
init(_ state: OngoingCallState) {
switch state {
case .initializing:
@ -335,7 +360,7 @@ private extension OngoingCallContextState {
}
}
private extension OngoingCallContextState {
private extension OngoingCallContextState.State {
init(_ state: OngoingCallStateWebrtc) {
switch state {
case .initializing:
@ -471,8 +496,25 @@ public final class OngoingCallContext {
})
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
context.stateChanged = { state in
self?.contextState.set(.single(OngoingCallContextState(state)))
context.stateChanged = { state, videoState in
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
self?.receptionPromise.set(.single(signalBars))
@ -498,7 +540,7 @@ public final class OngoingCallContext {
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
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
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> {
let poll = Signal<(String, String), NoError> { subscriber 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/video_codec",
"-Ithird-party/webrtc/webrtc-ios/src/third_party/libyuv/include",
"-Ithird-party/webrtc/webrtc-ios/src/sdk/objc/api/video_codec",
"-DWEBRTC_IOS",
"-DWEBRTC_MAC",
"-DWEBRTC_POSIX",

View File

@ -18,8 +18,9 @@ public:
void configurePlatformAudio();
std::unique_ptr<webrtc::VideoEncoderFactory> makeVideoEncoderFactory();
std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory();
bool supportsH265Encoding();
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
}

View File

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

View File

@ -1,5 +1,7 @@
#include "Manager.h"
#include "rtc_base/byte_buffer.h"
#ifdef TGVOIP_NAMESPACE
namespace TGVOIP_NAMESPACE {
#endif
@ -35,13 +37,16 @@ Manager::Manager(
TgVoipEncryptionKey encryptionKey,
bool enableP2P,
std::function<void (const TgVoipState &)> stateUpdated,
std::function<void (bool)> videoStateUpdated,
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
) :
_thread(thread),
_encryptionKey(encryptionKey),
_enableP2P(enableP2P),
_stateUpdated(stateUpdated),
_signalingDataEmitted(signalingDataEmitted) {
_videoStateUpdated(videoStateUpdated),
_signalingDataEmitted(signalingDataEmitted),
_isVideoRequested(false) {
assert(_thread->IsCurrent());
}
@ -87,7 +92,14 @@ void Manager::start() {
});
},
[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) {
_networkManager->perform([data](NetworkManager *networkManager) {
networkManager->receiveSignalingData(data);
rtc::CopyOnWriteBuffer buffer;
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,
bool enableP2P,
std::function<void (const TgVoipState &)> stateUpdated,
std::function<void (bool)> videoStateUpdated,
std::function<void (const std::vector<uint8_t> &)> signalingDataEmitted
);
~Manager();
void start();
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 setOutgoingVideoOutput(std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink);
@ -31,9 +34,11 @@ private:
TgVoipEncryptionKey _encryptionKey;
bool _enableP2P;
std::function<void (const TgVoipState &)> _stateUpdated;
std::function<void (bool)> _videoStateUpdated;
std::function<void (const std::vector<uint8_t> &)> _signalingDataEmitted;
std::unique_ptr<ThreadLocalObject<NetworkManager>> _networkManager;
std::unique_ptr<ThreadLocalObject<MediaManager>> _mediaManager;
bool _isVideoRequested;
private:
};

View File

@ -17,6 +17,8 @@
#include "api/video/video_bitrate_allocation.h"
#include "call/call.h"
#include "api/video_codecs/builtin_video_encoder_factory.h"
#if TARGET_OS_IPHONE
#include "CodecsApple.h"
@ -108,27 +110,50 @@ static std::vector<cricket::VideoCodec> AssignPayloadTypesAndDefaultCodecs(std::
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) {
bool useVP9 = false;
bool useH265 = true;
std::vector<cricket::VideoCodec> sortedCodecs;
for (auto &codec : codecs) {
if (useVP9) {
if (codec.name == cricket::kVp9CodecName) {
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);
}
if (sendCodecPriority(codec) != -1) {
sortedCodecs.push_back(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() {
@ -162,6 +187,14 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
_ssrcVideo.fecIncoming = 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));
_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_decoder_factory = webrtc::CreateAudioDecoderFactory<webrtc::AudioDecoderOpus>();
auto videoEncoderFactory = makeVideoEncoderFactory();
std::vector<cricket::VideoCodec> videoCodecs = AssignPayloadTypesAndDefaultCodecs(videoEncoderFactory->GetSupportedFormats());
mediaDeps.video_encoder_factory = makeVideoEncoderFactory();
mediaDeps.video_decoder_factory = makeVideoDecoderFactory();
@ -211,7 +241,6 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
const uint8_t opusMaxBitrateKbps = 32;
const uint8_t opusStartBitrateKbps = 6;
const uint8_t opusPTimeMs = 120;
const int extensionSequenceOne = 1;
cricket::AudioCodec opusCodec(opusSdpPayload, opusSdpName, opusClockrate, opusSdpBitrate, opusSdpChannels);
opusCodec.AddFeedbackParam(cricket::FeedbackParam(cricket::kRtcpFbParamTransportCc));
@ -223,7 +252,7 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
cricket::AudioSendParameters audioSendPrameters;
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.experimental_ns = false;
audioSendPrameters.options.noise_suppression = false;
@ -238,7 +267,7 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
cricket::AudioRecvParameters audioRecvParameters;
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.remote_estimate = true;
@ -246,75 +275,9 @@ _taskQueueFactory(webrtc::CreateDefaultTaskQueueFactory()) {
_audioChannel->AddRecvStream(cricket::StreamParams::CreateLegacy(_ssrcAudio.incoming));
_audioChannel->SetPlayout(true);
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->SetInterface(_videoNetworkInterface.get(), webrtc::MediaTransportConfig());
auto videoCodec = selectVideoCodec(videoCodecs);
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;*/
}
_nativeVideoSource = makeVideoSource(_thread, getWorkerThread());
}
MediaManager::~MediaManager() {
@ -334,18 +297,16 @@ MediaManager::~MediaManager() {
_audioChannel->SetInterface(nullptr, webrtc::MediaTransportConfig());
_videoChannel->RemoveRecvStream(_ssrcVideo.incoming);
_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());
setSendVideo(false);
}
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::VIDEO, webrtc::kNetworkUp);
} else {
@ -353,13 +314,13 @@ void MediaManager::setIsConnected(bool isConnected) {
_call->SignalChannelNetworkState(webrtc::MediaType::VIDEO, webrtc::kNetworkDown);
}
if (_audioChannel) {
_audioChannel->OnReadyToSend(isConnected);
_audioChannel->SetSend(isConnected);
_audioChannel->SetAudioSend(_ssrcAudio.outgoing, isConnected, nullptr, &_audioSource);
_audioChannel->OnReadyToSend(_isConnected);
_audioChannel->SetSend(_isConnected);
_audioChannel->SetAudioSend(_ssrcAudio.outgoing, _isConnected, nullptr, &_audioSource);
}
if (_videoChannel) {
_videoChannel->OnReadyToSend(isConnected);
_videoChannel->SetSend(isConnected);
if (_isSendingVideo && _videoChannel) {
_videoChannel->OnReadyToSend(_isConnected);
_videoChannel->SetSend(_isConnected);
}
}
@ -386,14 +347,113 @@ void MediaManager::notifyPacketSent(const rtc::SentPacket &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) {
_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) {
_currentOutgoingVideoSink = sink;
_nativeVideoSource->AddOrUpdateSink(sink.get(), rtc::VideoSinkWants());
_nativeVideoSource->AddOrUpdateSink(_currentOutgoingVideoSink.get(), rtc::VideoSinkWants());
}
MediaManager::NetworkInterfaceImpl::NetworkInterfaceImpl(MediaManager *mediaManager, bool isVideo) :

View File

@ -64,6 +64,8 @@ public:
void setIsConnected(bool isConnected);
void receivePacket(const rtc::CopyOnWriteBuffer &packet);
void notifyPacketSent(const rtc::SentPacket &sentPacket);
void setSendVideo(bool sendVideo);
void switchVideoCamera();
void setIncomingVideoOutput(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 _ssrcVideo;
bool _isConnected;
std::vector<cricket::VideoCodec> _videoCodecs;
bool _isSendingVideo;
bool _useFrontCamera;
std::unique_ptr<cricket::MediaEngineInterface> _mediaEngine;
std::unique_ptr<webrtc::Call> _call;
webrtc::FieldTrialBasedConfig _fieldTrials;

View File

@ -175,18 +175,17 @@ _signalingDataEmitted(signalingDataEmitted) {
flags |= cricket::PORTALLOCATOR_DISABLE_UDP;
flags |= cricket::PORTALLOCATOR_DISABLE_STUN;
}
//flags |= cricket::PORTALLOCATOR_DISABLE_UDP;
_portAllocator->set_flags(_portAllocator->flags() | flags);
_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;
stunServers.insert(defaultStunAddress);
std::vector<cricket::RelayServerConfig> turnServers;
turnServers.push_back(cricket::RelayServerConfig(
rtc::SocketAddress("hlgkfjdrtjfykgulhijkljhulyo.uksouth.cloudapp.azure.com", 3478),
"user",
"root",
rtc::SocketAddress("134.122.52.178", 3478),
"openrelay",
"openrelay",
cricket::PROTO_UDP
));
_portAllocator->SetConfiguration(stunServers, turnServers, 2, webrtc::NO_PRUNE);
@ -233,7 +232,7 @@ NetworkManager::~NetworkManager() {
_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());
uint32_t candidateCount = 0;
if (!reader.ReadUInt32(&candidateCount)) {

View File

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

View File

@ -138,6 +138,7 @@ public:
TgVoipNetworkType initialNetworkType,
TgVoipEncryptionKey const &encryptionKey,
std::function<void(TgVoipState)> stateUpdated,
std::function<void(bool)> videoStateUpdated,
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
);
@ -158,6 +159,8 @@ public:
virtual TgVoipPersistentState getPersistentState() = 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;
};

View File

@ -143,6 +143,7 @@ public:
TgVoipEncryptionKey const &encryptionKey,
TgVoipNetworkType initialNetworkType,
std::function<void(TgVoipState)> stateUpdated,
std::function<void(bool)> videoStateUpdated,
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
) :
_stateUpdated(stateUpdated),
@ -156,7 +157,7 @@ public:
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(
getManagerThread(),
encryptionKey,
@ -164,6 +165,9 @@ public:
[stateUpdated](const TgVoipState &state) {
stateUpdated(state);
},
[videoStateUpdated](bool isActive) {
videoStateUpdated(isActive);
},
[signalingDataEmitted](const std::vector<uint8_t> &data) {
signalingDataEmitted(data);
}
@ -183,6 +187,18 @@ public:
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 {
/*message::NetworkType mappedType;
@ -361,6 +377,7 @@ TgVoip *TgVoip::makeInstance(
TgVoipNetworkType initialNetworkType,
TgVoipEncryptionKey const &encryptionKey,
std::function<void(TgVoipState)> stateUpdated,
std::function<void(bool)> videoStateUpdated,
std::function<void(const std::vector<uint8_t> &)> signalingDataEmitted
) {
return new TgVoipImpl(
@ -371,6 +388,7 @@ TgVoip *TgVoip::makeInstance(
encryptionKey,
initialNetworkType,
stateUpdated,
videoStateUpdated,
signalingDataEmitted
);
}

View File

@ -23,6 +23,13 @@ typedef NS_ENUM(int32_t, OngoingCallStateWebrtc) {
OngoingCallStateReconnecting
};
typedef NS_ENUM(int32_t, OngoingCallVideoStateWebrtc) {
OngoingCallVideoStateInactive,
OngoingCallVideoStateRequesting,
OngoingCallVideoStateInvited,
OngoingCallVideoStateActive
};
typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) {
OngoingCallNetworkTypeWifi,
OngoingCallNetworkTypeCellularGprs,
@ -62,7 +69,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
+ (int32_t)maxLayer;
+ (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);
- (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;
- (void)setIsMuted:(bool)isMuted;
- (void)setVideoEnabled:(bool)videoEnabled;
- (void)switchVideoCamera;
- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType;
- (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;
- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;

View File

@ -34,6 +34,8 @@ using namespace TGVOIP_NAMESPACE;
TgVoip *_tgVoip;
OngoingCallStateWebrtc _state;
OngoingCallVideoStateWebrtc _videoState;
int32_t _signalBars;
NSData *_lastDerivedState;
@ -127,6 +129,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
_callPacketTimeout = 10.0;
_networkType = networkType;
_sendSignalingData = [sendSignalingData copy];
_videoState = OngoingCallVideoStateInactive;
std::vector<uint8_t> derivedStateValue;
derivedStateValue.resize(derivedState.length);
@ -172,7 +175,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
.initializationTimeout = _callConnectTimeout,
.receiveTimeout = _callPacketTimeout,
.dataSaving = callControllerDataSavingForType(dataSaving),
.enableP2P = allowP2P,
.enableP2P = (bool)allowP2P,
.enableAEC = false,
.enableNS = 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) {
NSData *mappedData = [[NSData alloc] initWithBytes:data.data() length:data.size()];
[queue dispatch:^{
@ -300,7 +322,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
_state = callState;
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 {
if (_networkType != networkType) {
_networkType = networkType;

View File

@ -15,16 +15,38 @@ genrule(
set -x
echo "SRCDIR=$SRCDIR"
OUT_DIR="ios"
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"
mkdir -p "$BUILD_DIR"
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"
DEPOT_TOOLS_PATH="$(location //third-party:depot_tools_sources)"
@ -39,7 +61,7 @@ genrule(
sh $SRCDIR/build-webrtc-buck.sh "$BUILD_DIR" $BUILD_ARCH
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",
visibility = ["PUBLIC"]

View File

@ -35,7 +35,7 @@ genrule(
echo "Unsupported architecture $(TARGET_CPU)"
fi
BUILD_DIR="$(RULEDIR)/$$BUILD_ARCH"
rm -rf "$$BUILD_DIR"
#rm -rf "$$BUILD_DIR"
mkdir -p "$$BUILD_DIR"
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/
OUT_DIR="ios"
if [ "$ARCH" == "x64" ]; then
if [ "$ARCH" == "arm64" ]; then
OUT_DIR="ios_64"
elif [ "$ARCH" == "x64" ]; then
OUT_DIR="ios_sim"
fi