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

This commit is contained in:
Ilya Laktyushin 2020-12-04 18:29:34 +04:00
commit 4c27869b04
19 changed files with 4154 additions and 3911 deletions

View File

@ -5940,6 +5940,7 @@ Sorry for the inconvenience.";
"VoiceChat.EndVoiceChat" = "End Voice Chat";
"VoiceChat.CopyInviteLink" = "Copy Invite Link";
"VoiceChat.InviteLinkCopiedText" = "Invite link copied to clipboard";
"VoiceChat.UnmutePeer" = "Allow to Speak";
"VoiceChat.MutePeer" = "Mute";
@ -5956,6 +5957,9 @@ Sorry for the inconvenience.";
"Notification.VoiceChatInvitation" = "%1$@ invited %2$@ to the voice chat";
"Notification.VoiceChatInvitationForYou" = "%1$@ invited you to the voice chat";
"VoiceChat.InvitedPeerText" = "You invited %@ to the voice chat";
"VoiceChat.RemovedPeerText" = "You removed %@ from this group";
"Notification.VoiceChatStarted" = "Voice chat started";
"Notification.VoiceChatEnded" = "Voice chat ended (%@)";

View File

@ -286,6 +286,7 @@ public protocol PresentationGroupCall: class {
func updateMuteState(peerId: PeerId, isMuted: Bool)
func invitePeer(_ peerId: PeerId)
func removedPeer(_ peerId: PeerId)
var invitedPeers: Signal<[PeerId], NoError> { get }
var sourcePanel: ASDisplayNode? { get set }

View File

@ -58,6 +58,7 @@ private let avatarFont = avatarPlaceholderFont(size: 12.0)
private final class ContentNode: ASDisplayNode {
private var audioLevelView: VoiceBlobView?
private var audioLevelBlobOverlay: UIImageView?
private let unclippedNode: ASImageNode
private let clippedNode: ASImageNode
@ -136,7 +137,7 @@ private final class ContentNode: ASDisplayNode {
}
}
func updateAudioLevel(color: UIColor, value: Float) {
func updateAudioLevel(color: UIColor, backgroundColor: UIColor, value: Float) {
if self.audioLevelView == nil, value > 0.0 {
let blobFrame = self.unclippedNode.bounds.insetBy(dx: -8.0, dy: -8.0)
@ -248,12 +249,12 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
return CGSize(width: contentWidth, height: contentHeight)
}
public func updateAudioLevels(color: UIColor, levels: [PeerId: Float]) {
public func updateAudioLevels(color: UIColor, backgroundColor: UIColor, levels: [PeerId: Float]) {
for (key, itemNode) in self.contentNodes {
if let value = levels[key.peerId] {
itemNode.updateAudioLevel(color: color, value: value)
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: value)
} else {
itemNode.updateAudioLevel(color: color, value: 0.0)
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: 0.0)
}
}
}

View File

@ -14,16 +14,16 @@ public enum ContactListActionItemHighlight {
case alpha
}
class ContactListActionItem: ListViewItem, ListViewItemWithHeader {
public class ContactListActionItem: ListViewItem, ListViewItemWithHeader {
let presentationData: ItemListPresentationData
let title: String
let icon: ContactListActionItemIcon
let highlight: ContactListActionItemHighlight
let clearHighlightAutomatically: Bool
let action: () -> Void
let header: ListViewItemHeader?
public let header: ListViewItemHeader?
init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) {
public init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) {
self.presentationData = presentationData
self.title = title
self.icon = icon
@ -33,7 +33,7 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader {
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ContactListActionItemNode()
let (_, last, firstWithHeader) = ContactListActionItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
@ -50,7 +50,7 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader {
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ContactListActionItemNode {
let makeLayout = nodeValue.asyncLayout()
@ -68,9 +68,9 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader {
}
}
var selectable: Bool = true
public var selectable: Bool = true
func selected(listView: ListView){
public func selected(listView: ListView){
self.action()
if self.clearHighlightAutomatically {
listView.clearHighlightAnimated(true)

View File

@ -438,6 +438,12 @@ open class NavigationController: UINavigationController, ContainableController,
modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
modalContainer.container.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
self.modalContainers.append(modalContainer)
if !modalContainer.isReady {
modalContainer.isReadyUpdated = { [weak self, weak modalContainer] in
@ -586,6 +592,7 @@ open class NavigationController: UINavigationController, ContainableController,
}
var previousModalContainer: NavigationModalContainer?
var topVisibleModalContainerWithStatusBar: NavigationModalContainer?
var visibleModalCount = 0
var topModalIsFlat = false
let isLandscape = layout.orientation == .landscape
@ -617,7 +624,7 @@ open class NavigationController: UINavigationController, ContainableController,
}
containerTransition.updateFrame(node: modalContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
modalContainer.update(layout: layout, controllers: navigationLayout.modal[i].controllers, coveredByModalTransition: effectiveModalTransition, transition: containerTransition)
modalContainer.update(layout: modalContainer.isFlat ? overlayLayout : layout, controllers: navigationLayout.modal[i].controllers, coveredByModalTransition: effectiveModalTransition, transition: containerTransition)
if modalContainer.supernode == nil && modalContainer.isReady {
if let previousModalContainer = previousModalContainer {
@ -653,6 +660,23 @@ open class NavigationController: UINavigationController, ContainableController,
modalContainer.keyboardViewManager = nil
modalContainer.canHaveKeyboardFocus = true
}
if modalContainer.isFlat {
let controllerStatusBarStyle = modalContainer.container.statusBarStyle
switch controllerStatusBarStyle {
case .Black, .White, .Hide:
if topVisibleModalContainerWithStatusBar == nil {
topVisibleModalContainerWithStatusBar = modalContainer
}
if case .Hide = controllerStatusBarStyle {
statusBarHidden = true
} else {
statusBarHidden = false
}
case .Ignore:
break
}
}
} else {
modalContainer.keyboardViewManager = nil
modalContainer.canHaveKeyboardFocus = false
@ -921,6 +945,10 @@ open class NavigationController: UINavigationController, ContainableController,
statusBarStyle = topVisibleOverlayContainerWithStatusBar.controller.statusBar.statusBarStyle
}
if let topVisibleModalContainerWithStatusBar = topVisibleModalContainerWithStatusBar {
statusBarStyle = topVisibleModalContainerWithStatusBar.container.statusBarStyle
}
if self.currentStatusBarExternalHidden {
statusBarHidden = true
}

View File

@ -372,7 +372,14 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
topInset += statusBarHeight
}
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let effectiveStatusBarHeight: CGFloat?
if self.isFlat {
effectiveStatusBarHeight = layout.statusBarHeight
} else {
effectiveStatusBarHeight = nil
}
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
@ -410,7 +417,15 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
if let inputHeightValue = layout.inputHeight {
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
}
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let effectiveStatusBarHeight: CGFloat?
if self.isFlat {
effectiveStatusBarHeight = layout.statusBarHeight
} else {
effectiveStatusBarHeight = nil
}
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
}
transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame.offsetBy(dx: 0.0, dy: layout.size.height))
transition.updateTransformScale(node: self.container, scale: containerScale)

View File

@ -30,6 +30,8 @@ public final class ChannelMembersSearchController: ViewController {
private let filters: [ChannelMembersSearchFilter]
private let openPeer: (Peer, RenderedChannelParticipant?) -> Void
public var copyInviteLink: (() -> Void)?
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData
@ -93,6 +95,9 @@ public final class ChannelMembersSearchController: ViewController {
self.controllerNode.requestOpenPeerFromSearch = { [weak self] peer, participant in
self?.openPeer(peer, participant)
}
self.controllerNode.requestCopyInviteLink = { [weak self] in
self?.copyInviteLink?()
}
self.controllerNode.pushController = { [weak self] c in
(self?.navigationController as? NavigationController)?.pushViewController(c)
}

View File

@ -15,63 +15,98 @@ import SearchBarNode
import ContactsPeerItem
import SearchUI
import ItemListUI
import ContactListUI
import ChatListSearchItemHeader
private final class ChannelMembersSearchInteraction {
let openPeer: (Peer, RenderedChannelParticipant?) -> Void
let copyInviteLink: () -> Void
init(openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) {
init(
openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void,
copyInviteLink: @escaping () -> Void
) {
self.openPeer = openPeer
self.copyInviteLink = copyInviteLink
}
}
private enum ChannelMembersSearchEntryId: Hashable {
case copyInviteLink
case peer(PeerId)
}
private enum ChannelMembersSearchEntry: Comparable, Identifiable {
case copyInviteLink
case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing, String?, Bool)
var stableId: ChannelMembersSearchEntryId {
switch self {
case let .peer(peer):
return .peer(peer.1.peer.id)
case .copyInviteLink:
return .copyInviteLink
case let .peer(_, participant, _, _, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool {
switch lhs {
case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled):
if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled) = rhs {
return true
} else {
return false
}
case .copyInviteLink:
if case .copyInviteLink = rhs {
return true
} else {
return false
}
case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled):
if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool {
switch lhs {
case let .peer(lhsPeer):
if case let .peer(rhsPeer) = rhs {
return lhsPeer.0 < rhsPeer.0
} else {
return false
}
case .copyInviteLink:
if case .copyInviteLink = rhs {
return false
} else {
return true
}
case let .peer(lhsIndex, _, _, _, _):
if case .copyInviteLink = rhs {
return false
} else if case let .peer(rhsIndex, _, _, _, _) = rhs {
return lhsIndex < rhsIndex
} else {
return false
}
}
}
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ListViewItem {
switch self {
case let .peer(_, participant, editing, label, enabled):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: label, multiline: false)
} else {
status = .none
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in
interaction.openPeer(participant.peer, participant)
})
case .copyInviteLink:
let icon: ContactListActionItemIcon
if let iconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: presentationData.theme.list.itemAccentColor) {
icon = .generic(iconImage)
} else {
icon = .none
}
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.VoiceChat_CopyInviteLink, icon: icon, clearHighlightAutomatically: true, header: nil, action: {
interaction.copyInviteLink()
})
case let .peer(_, participant, editing, label, enabled):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: label, multiline: false)
} else {
status = .none
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: .members, theme: presentationData.theme, strings: presentationData.strings), action: { _ in
interaction.openPeer(participant.peer, participant)
})
}
}
}
@ -110,6 +145,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
var requestActivateSearch: (() -> Void)?
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((Peer, RenderedChannelParticipant?) -> Void)?
var requestCopyInviteLink: (() -> Void)?
var pushController: ((ViewController) -> Void)?
private let forceTheme: PresentationTheme?
@ -140,10 +176,16 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
self.addSubnode(self.listNode)
let interaction = ChannelMembersSearchInteraction(openPeer: { [weak self] peer, participant in
self?.requestOpenPeerFromSearch?(peer, participant)
self?.listNode.clearHighlightAnimated(true)
})
let interaction = ChannelMembersSearchInteraction(
openPeer: { [weak self] peer, participant in
self?.requestOpenPeerFromSearch?(peer, participant)
self?.listNode.clearHighlightAnimated(true)
},
copyInviteLink: { [weak self] in
self?.requestCopyInviteLink?()
self?.listNode.clearHighlightAnimated(true)
}
)
let previousEntries = Atomic<[ChannelMembersSearchEntry]?>(value: nil)
@ -274,6 +316,16 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
}
var entries: [ChannelMembersSearchEntry] = []
if case .inviteToCall = mode, !filters.contains(where: { filter in
if case .excludeNonMembers = filter {
return true
} else {
return false
}
}) {
entries.append(.copyInviteLink)
}
var index = 0
for participant in state.list {
if participant.peer.isDeleted {

View File

@ -26,155 +26,6 @@ public enum LocationBroadcastPanelSource {
case peer(PeerId)
}
final class AccountGroupCallContextImpl: AccountGroupCallContext {
final class Proxy {
let context: AccountGroupCallContextImpl
let removed: () -> Void
init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) {
self.context = context
self.removed = removed
}
deinit {
self.removed()
}
func keep() {
}
}
var disposable: Disposable?
var participantsContext: GroupCallParticipantsContext?
private let panelDataPromise = Promise<GroupCallPanelData>()
var panelData: Signal<GroupCallPanelData, NoError> {
return self.panelDataPromise.get()
}
init(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) {
self.panelDataPromise.set(.single(GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(
id: call.id,
accessHash: call.accessHash,
participantCount: 0,
clientParams: nil
),
topParticipants: [],
participantCount: 0,
activeSpeakers: Set(),
groupCall: nil
)))
self.disposable = (getGroupCallParticipants(account: account, callId: call.id, accessHash: call.accessHash, offset: "", limit: 100)
|> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self, let state = state else {
return
}
let context = GroupCallParticipantsContext(
account: account,
peerId: peerId,
id: call.id,
accessHash: call.accessHash,
state: state
)
strongSelf.participantsContext = context
strongSelf.panelDataPromise.set(combineLatest(queue: .mainQueue(),
context.state,
context.activeSpeakers
)
|> map { state, activeSpeakers -> GroupCallPanelData in
var topParticipants: [GroupCallParticipantsContext.Participant] = []
for participant in state.participants {
if topParticipants.count >= 3 {
break
}
topParticipants.append(participant)
}
return GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil),
topParticipants: topParticipants,
participantCount: state.totalCount,
activeSpeakers: activeSpeakers,
groupCall: nil
)
})
})
}
deinit {
self.disposable?.dispose()
}
}
public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache {
class Impl {
private class Record {
let context: AccountGroupCallContextImpl
let subscribers = Bag<Void>()
var removeTimer: SwiftSignalKit.Timer?
init(context: AccountGroupCallContextImpl) {
self.context = context
}
}
private let queue: Queue
private var contexts: [Int64: Record] = [:]
init(queue: Queue) {
self.queue = queue
}
func get(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy {
let result: Record
if let current = self.contexts[call.id] {
result = current
} else {
let context = AccountGroupCallContextImpl(account: account, peerId: peerId, call: call)
result = Record(context: context)
self.contexts[call.id] = result
}
let index = result.subscribers.add(Void())
result.removeTimer?.invalidate()
result.removeTimer = nil
return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in
Queue.mainQueue().async {
if let strongResult = result, let strongSelf = self, strongSelf.contexts[call.id] === strongResult {
strongResult.subscribers.remove(index)
if strongResult.subscribers.isEmpty {
let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: {
if let result = result, let strongSelf = self, strongSelf.contexts[call.id] === result, result.subscribers.isEmpty {
strongSelf.contexts.removeValue(forKey: call.id)
}
}, queue: .mainQueue())
strongResult.removeTimer = removeTimer
removeTimer.start()
}
}
}
})
}
}
let queue: Queue = .mainQueue()
let impl: QueueLocalObject<Impl>
public init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue)
})
}
}
private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) {
let presentImpl: (Message?) -> Void = { [weak controller] message in
if let message = message, let strongController = controller {

View File

@ -426,7 +426,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
if self.audioLevelGenerators.isEmpty {
self.audioLevelGeneratorTimer?.invalidate()
self.audioLevelGeneratorTimer = nil
self.avatarsNode.updateAudioLevels(color: self.theme.chat.inputPanel.actionControlFillColor, levels: [:])
self.avatarsNode.updateAudioLevels(color: self.theme.chat.inputPanel.actionControlFillColor, backgroundColor: self.theme.chat.inputPanel.actionControlFillColor, levels: [:])
} else if self.audioLevelGeneratorTimer == nil {
let audioLevelGeneratorTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in
self?.sampleAudioGenerators()
@ -442,7 +442,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
for (peerId, generator) in self.audioLevelGenerators {
levels[peerId] = generator.get()
}
self.avatarsNode.updateAudioLevels(color: self.theme.chat.inputPanel.actionControlFillColor, levels: levels)
self.avatarsNode.updateAudioLevels(color: self.theme.chat.inputPanel.actionControlFillColor, backgroundColor: self.theme.chat.inputPanel.actionControlFillColor, levels: levels)
}
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -15,6 +15,155 @@ import DeviceAccess
import UniversalMediaPlayer
import AccountContext
public final class AccountGroupCallContextImpl: AccountGroupCallContext {
public final class Proxy {
public let context: AccountGroupCallContextImpl
let removed: () -> Void
public init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) {
self.context = context
self.removed = removed
}
deinit {
self.removed()
}
public func keep() {
}
}
var disposable: Disposable?
public var participantsContext: GroupCallParticipantsContext?
private let panelDataPromise = Promise<GroupCallPanelData>()
public var panelData: Signal<GroupCallPanelData, NoError> {
return self.panelDataPromise.get()
}
public init(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) {
self.panelDataPromise.set(.single(GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(
id: call.id,
accessHash: call.accessHash,
participantCount: 0,
clientParams: nil
),
topParticipants: [],
participantCount: 0,
activeSpeakers: Set(),
groupCall: nil
)))
self.disposable = (getGroupCallParticipants(account: account, callId: call.id, accessHash: call.accessHash, offset: "", limit: 100)
|> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self, let state = state else {
return
}
let context = GroupCallParticipantsContext(
account: account,
peerId: peerId,
id: call.id,
accessHash: call.accessHash,
state: state
)
strongSelf.participantsContext = context
strongSelf.panelDataPromise.set(combineLatest(queue: .mainQueue(),
context.state,
context.activeSpeakers
)
|> map { state, activeSpeakers -> GroupCallPanelData in
var topParticipants: [GroupCallParticipantsContext.Participant] = []
for participant in state.participants {
if topParticipants.count >= 3 {
break
}
topParticipants.append(participant)
}
return GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil),
topParticipants: topParticipants,
participantCount: state.totalCount,
activeSpeakers: activeSpeakers,
groupCall: nil
)
})
})
}
deinit {
self.disposable?.dispose()
}
}
public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache {
public class Impl {
private class Record {
let context: AccountGroupCallContextImpl
let subscribers = Bag<Void>()
var removeTimer: SwiftSignalKit.Timer?
init(context: AccountGroupCallContextImpl) {
self.context = context
}
}
private let queue: Queue
private var contexts: [Int64: Record] = [:]
init(queue: Queue) {
self.queue = queue
}
public func get(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy {
let result: Record
if let current = self.contexts[call.id] {
result = current
} else {
let context = AccountGroupCallContextImpl(account: account, peerId: peerId, call: call)
result = Record(context: context)
self.contexts[call.id] = result
}
let index = result.subscribers.add(Void())
result.removeTimer?.invalidate()
result.removeTimer = nil
return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in
Queue.mainQueue().async {
if let strongResult = result, let strongSelf = self, strongSelf.contexts[call.id] === strongResult {
strongResult.subscribers.remove(index)
if strongResult.subscribers.isEmpty {
let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: {
if let result = result, let strongSelf = self, strongSelf.contexts[call.id] === result, result.subscribers.isEmpty {
strongSelf.contexts.removeValue(forKey: call.id)
}
}, queue: .mainQueue())
strongResult.removeTimer = removeTimer
removeTimer.start()
}
}
}
})
}
}
let queue: Queue = .mainQueue()
public let impl: QueueLocalObject<Impl>
public init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue)
})
}
}
private extension PresentationGroupCallState {
static var initialValue: PresentationGroupCallState {
return PresentationGroupCallState(
@ -437,6 +586,50 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
)
})
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash))
}) {
if let participantsContext = temporaryParticipantsContext.context.participantsContext {
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
participantsContext.state,
participantsContext.activeSpeakers
).start(next: { [weak self] state, activeSpeakers in
guard let strongSelf = self else {
return
}
var topParticipants: [GroupCallParticipantsContext.Participant] = []
var members = PresentationGroupCallMembers(
participants: [],
speakingParticipants: [],
totalCount: 0,
loadMoreToken: nil
)
for participant in state.participants {
members.participants.append(participant)
if topParticipants.count < 3 {
topParticipants.append(participant)
}
}
members.totalCount = state.totalCount
members.loadMoreToken = state.nextParticipantsFetchOffset
strongSelf.membersValue = members
strongSelf.stateValue.adminIds = state.adminIds
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: state.totalCount,
topParticipants: topParticipants,
activeSpeakers: activeSpeakers
)))
}))
}
}
self.requestCall()
}
@ -922,6 +1115,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()
}
public func removedPeer(_ peerId: PeerId) {
var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.removeAll(where: { $0 == peerId})
self.invitedPeersValue = updatedInvitedPeers
}
private var currentMyAudioLevel: Float = 0.0
private var currentMyAudioLevelTimestamp: Double = 0.0
private var isSendingTyping: Bool = false

View File

@ -556,6 +556,8 @@ public final class VoiceChatController: ViewController {
if let participant = participant {
strongSelf.call.invitePeer(participant.peer.id)
dismissController?()
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current)
} else {
let selfController = strongSelf.controller
let inviteDisposable = strongSelf.inviteDisposable
@ -586,14 +588,7 @@ public final class VoiceChatController: ViewController {
inviteDisposable.set(nil)
}
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(next: { _ in
guard let strongSelf = self else {
dismissController?()
return
}
strongSelf.call.invitePeer(peer.id)
dismissController?()
}, error: { error in
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in
dismissController?()
guard let strongSelf = self else {
return
@ -620,9 +615,49 @@ public final class VoiceChatController: ViewController {
text = presentationData.strings.Login_UnknownError
}
strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: {
guard let strongSelf = self else {
dismissController?()
return
}
strongSelf.call.invitePeer(peer.id)
dismissController?()
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current)
}))
}
})
controller.copyInviteLink = {
dismissController?()
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> String? in
if let peer = transaction.getPeer(call.peerId), let addressName = peer.addressName, !addressName.isEmpty {
return "https://t.me/\(addressName)"
} else if let cachedData = transaction.getPeerCachedData(peerId: call.peerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation?.link
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation?.link
}
}
return nil
}
|> deliverOnMainQueue).start(next: { link in
guard let strongSelf = self else {
return
}
if let link = link {
UIPasteboard.general.string = link
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .linkCopied(text: strongSelf.presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
}
dismissController = { [weak controller] in
controller?.dismiss()
}
@ -682,7 +717,14 @@ public final class VoiceChatController: ViewController {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: strongSelf.context.account, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
strongSelf.call.removedPeer(peer.id)
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .banned(text: strongSelf.presentationData.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current)
}))
actionSheet.setItemGroups([

View File

@ -39,7 +39,7 @@ func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo,
public func cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool) -> Signal<CachedStickerPackResult, NoError> {
return postbox.transaction { transaction -> CachedStickerPackResult? in
if let (info, items, local) = cachedStickerPack(transaction: transaction, reference: reference) {
if local {
if local && !forceRemote {
return .result(info, items, true)
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,8 @@ public enum UndoOverlayContent {
case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool)
case setProximityAlert(title: String, text: String, cancelled: Bool)
case invitedToVoiceChat(context: AccountContext, peer: Peer, text: String)
case linkCopied(text: String)
case banned(text: String)
}
public enum UndoOverlayAction {

View File

@ -182,6 +182,34 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = true
undoText = cancel
self.originalRemainingSeconds = 5
case let .linkCopied(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_linkcopied", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
self.animatedStickerNode = nil
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 5
case let .banned(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_banned", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
self.animatedStickerNode = nil
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 5
case let .chatAddedToFolder(chatTitle, folderTitle):
self.avatarNode = nil
self.iconNode = nil
@ -467,7 +495,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content {
case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat:
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned:
break
case .dice:
self.panelWrapperNode.clipsToBounds = true
@ -579,6 +607,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
if case .messagesUnpinned = self.content {
let factor: CGFloat = 0.5
preferredSize = CGSize(width: floor(iconSize.width * factor), height: floor(iconSize.height * factor))
} else if case .linkCopied = self.content {
let factor: CGFloat = 0.08
preferredSize = CGSize(width: floor(iconSize.width * factor), height: floor(iconSize.height * factor))
} else if case .banned = self.content {
let factor: CGFloat = 0.08
preferredSize = CGSize(width: floor(iconSize.width * factor), height: floor(iconSize.height * factor))
} else {
preferredSize = iconSize
}