mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
74a4b18a17
2
Makefile
2
Makefile
@ -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)
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -628,7 +628,7 @@ typedef enum
|
||||
_dismissed = cancelled;
|
||||
|
||||
if (self.onDismiss != nil)
|
||||
self.onDismiss(_automaticDismiss);
|
||||
self.onDismiss(_automaticDismiss, cancelled);
|
||||
|
||||
if (_player != nil)
|
||||
[_player pause];
|
||||
|
@ -317,6 +317,8 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
||||
}
|
||||
|
||||
private func dismiss(forceAway: Bool) {
|
||||
self.animatedIn.set(false)
|
||||
|
||||
var animatedOutNode = true
|
||||
var animatedOutInterface = false
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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?()
|
||||
|
@ -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 {
|
||||
|
@ -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, "")))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
};
|
||||
|
@ -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) :
|
||||
|
@ -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;
|
||||
|
@ -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)) {
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
32
third-party/webrtc/BUCK
vendored
32
third-party/webrtc/BUCK
vendored
@ -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"]
|
||||
|
2
third-party/webrtc/BUILD
vendored
2
third-party/webrtc/BUILD
vendored
@ -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"
|
||||
|
4
third-party/webrtc/build-webrtc-buck.sh
vendored
4
third-party/webrtc/build-webrtc-buck.sh
vendored
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user