Video Chat Improvements

This commit is contained in:
Ilya Laktyushin 2021-06-12 18:11:26 +03:00
parent 3b34ee26b7
commit f36bba8143
16 changed files with 321 additions and 88 deletions

View File

@ -1217,7 +1217,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
})) }))
self.suggestAutoarchiveDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox) self.suggestAutoarchiveDisposable.set((getServerProvidedSuggestions(account: self.context.account)
|> deliverOnMainQueue).start(next: { [weak self] values in |> deliverOnMainQueue).start(next: { [weak self] values in
guard let strongSelf = self else { guard let strongSelf = self else {
return return

View File

@ -857,7 +857,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
} }
let intermediateCompletion: () -> Void = { [weak contentParentNode] in let intermediateCompletion: () -> Void = { [weak self, weak contentParentNode] in
if completedEffect && completedContentNode && completedActionsNode { if completedEffect && completedContentNode && completedActionsNode {
switch result { switch result {
case .default, .custom: case .default, .custom:
@ -870,6 +870,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
break break
} }
self?.clippingNode.view.mask = nil
completion() completion()
} }
} }

View File

@ -111,7 +111,7 @@ public class ItemListInfoItem: InfoListItem, ItemListItem {
} }
} }
class InfoItemNode: ListViewItemNode { public class InfoItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -128,7 +128,7 @@ class InfoItemNode: ListViewItemNode {
private var item: InfoListItem? private var item: InfoListItem?
override var canBeSelected: Bool { public override var canBeSelected: Bool {
return false return false
} }
@ -178,7 +178,7 @@ class InfoItemNode: ListViewItemNode {
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
} }
override func didLoad() { public override func didLoad() {
super.didLoad() super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
@ -204,12 +204,12 @@ class InfoItemNode: ListViewItemNode {
let currentItem = self.item let currentItem = self.item
return { item, params, neighbors in return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = 15.0 + params.rightInset let rightInset: CGFloat = 16.0 + params.rightInset
let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize) let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let textFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) let textFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0)
let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize) let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0)
let badgeFont = Font.regular(15.0) let badgeFont = Font.regular(15.0)
var updatedTheme: PresentationTheme? var updatedTheme: PresentationTheme?
@ -217,7 +217,7 @@ class InfoItemNode: ListViewItemNode {
var updatedCloseIcon: UIImage? var updatedCloseIcon: UIImage?
let badgeDiameter: CGFloat = 20.0 let badgeDiameter: CGFloat = 22.0
if currentItem?.presentationData.theme !== item.presentationData.theme { if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme updatedTheme = item.presentationData.theme
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.presentationData.theme.list.itemDestructiveColor) updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.presentationData.theme.list.itemDestructiveColor)
@ -254,11 +254,11 @@ class InfoItemNode: ListViewItemNode {
})) }))
} }
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 36.0) let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 38.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in return (layout, { [weak self] in
@ -338,11 +338,11 @@ class InfoItemNode: ListViewItemNode {
strongSelf.closeButton.setImage(updatedCloseIcon, for: []) strongSelf.closeButton.setImage(updatedCloseIcon, for: [])
} }
strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0 + floor((titleLayout.size.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeDiameter, height: badgeDiameter)) strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0), size: CGSize(width: badgeDiameter, height: badgeDiameter))
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 1.0), size: labelLayout.size) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 2.0 + UIScreenPixel), size: labelLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.maxX + 8.0, y: 15.0), size: titleLayout.size) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.maxX + 8.0, y: 16.0), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 9.0), size: textLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 9.0), size: textLayout.size)
@ -352,15 +352,15 @@ class InfoItemNode: ListViewItemNode {
} }
} }
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { public override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
} }
override func animateAdded(_ currentTimestamp: Double, duration: Double) { public override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
override func animateRemoved(_ currentTimestamp: Double, duration: Double) { public override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
} }

View File

@ -277,6 +277,9 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin
} }
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil) presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil)
let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePhoneNumber).start()
dismissImpl?() dismissImpl?()
})) }))
} }

View File

@ -11,6 +11,7 @@ import AlertUI
import PresentationDataUtils import PresentationDataUtils
import AppBundle import AppBundle
import Markdown import Markdown
import PhoneNumberFormat
private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
var presentationData: PresentationData var presentationData: PresentationData
@ -101,7 +102,8 @@ public final class ChangePhoneNumberIntroController: ViewController {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = phoneNumber let formattedPhone = formatPhoneNumber(phoneNumber)
self.title = formattedPhone
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
//self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)) //self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed))
} }

View File

@ -399,6 +399,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var temporaryActivityRank: Int? private var temporaryActivityRank: Int?
private var temporaryRaiseHandRating: Int64? private var temporaryRaiseHandRating: Int64?
private var temporaryHasRaiseHand: Bool = false private var temporaryHasRaiseHand: Bool = false
private var temporaryJoinedVideo: Bool = true
private var temporaryMuteState: GroupCallParticipantsContext.Participant.MuteState? private var temporaryMuteState: GroupCallParticipantsContext.Participant.MuteState?
private var internalState: InternalState = .requesting private var internalState: InternalState = .requesting
@ -1016,7 +1017,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
activityRank: strongSelf.temporaryActivityRank, activityRank: strongSelf.temporaryActivityRank,
muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
volume: nil, volume: nil,
about: about about: about,
joinedVideo: strongSelf.temporaryJoinedVideo
)) ))
participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) })
} }
@ -1097,7 +1099,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
activityRank: strongSelf.temporaryActivityRank, activityRank: strongSelf.temporaryActivityRank,
muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
volume: nil, volume: nil,
about: about about: about,
joinedVideo: strongSelf.temporaryJoinedVideo
)) ))
} }
@ -1261,7 +1264,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
activityRank: strongSelf.temporaryActivityRank, activityRank: strongSelf.temporaryActivityRank,
muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canManageCall || !state.defaultParticipantsAreMuted.isMuted, mutedByYou: false), muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canManageCall || !state.defaultParticipantsAreMuted.isMuted, mutedByYou: false),
volume: nil, volume: nil,
about: about about: about,
joinedVideo: strongSelf.temporaryJoinedVideo
)) ))
} }
@ -1828,7 +1832,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
activityRank: strongSelf.temporaryActivityRank, activityRank: strongSelf.temporaryActivityRank,
muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
volume: nil, volume: nil,
about: about about: about,
joinedVideo: strongSelf.temporaryJoinedVideo
)) ))
participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) })
} }
@ -2213,6 +2218,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.temporaryRaiseHandRating = participant.raiseHandRating strongSelf.temporaryRaiseHandRating = participant.raiseHandRating
strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand
strongSelf.temporaryMuteState = participant.muteState strongSelf.temporaryMuteState = participant.muteState
strongSelf.temporaryJoinedVideo = participant.joinedVideo
} }
} }
strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId) strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId)

View File

@ -1441,7 +1441,7 @@ public final class VoiceChatController: ViewController {
strongSelf.controller?.push(controller) strongSelf.controller?.push(controller)
}) })
}, peerContextAction: { [weak self] entry, sourceNode, gesture, fullscreen in }, peerContextAction: { [weak self] entry, sourceNode, gesture, fullscreen in
guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { guard let strongSelf = self, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
return return
} }
@ -1723,14 +1723,20 @@ public final class VoiceChatController: ViewController {
centerVertically = false centerVertically = false
} }
var useMaskView = true
if case .fullscreen = strongSelf.displayMode {
useMaskView = false
}
let dismissPromise = ValuePromise<Bool>(false) let dismissPromise = ValuePromise<Bool>(false)
let source = VoiceChatContextExtractedContentSource(sourceNode: sourceNode, maskView: strongSelf.transitionMaskView, keepInPlace: false, blurBackground: true, centerVertically: centerVertically, shouldBeDismissed: dismissPromise.get(), animateTransitionIn: { [weak self] in let source = VoiceChatContextExtractedContentSource(sourceNode: sourceNode, maskView: useMaskView ? strongSelf.transitionMaskView : nil, keepInPlace: false, blurBackground: true, centerVertically: centerVertically, shouldBeDismissed: dismissPromise.get(), animateTransitionIn: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.animatingContextMenu = true strongSelf.animatingContextMenu = true
strongSelf.updateDecorationsLayout(transition: .immediate) strongSelf.updateDecorationsLayout(transition: .immediate)
if strongSelf.isLandscape { if strongSelf.isLandscape {
strongSelf.transitionMaskTopFillLayer.opacity = 1.0 strongSelf.transitionMaskTopFillLayer.opacity = 1.0
} }
strongSelf.transitionContainerNode.view.mask = nil
strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in
Queue.mainQueue().after(0.3) { Queue.mainQueue().after(0.3) {
self?.transitionMaskTopFillLayer.opacity = 0.0 self?.transitionMaskTopFillLayer.opacity = 0.0
@ -1748,6 +1754,7 @@ public final class VoiceChatController: ViewController {
strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, completion: { [weak self] _ in strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, completion: { [weak self] _ in
self?.animatingContextMenu = false self?.animatingContextMenu = false
self?.updateDecorationsLayout(transition: .immediate) self?.updateDecorationsLayout(transition: .immediate)
self?.transitionContainerNode.view.mask = self?.transitionMaskView
}) })
} }
}) })
@ -4737,6 +4744,8 @@ public final class VoiceChatController: ViewController {
displayPanelVideos = self.displayPanelVideos displayPanelVideos = self.displayPanelVideos
} }
var joinedVideo = true
var myEntry: VoiceChatPeerEntry? var myEntry: VoiceChatPeerEntry?
var mainEntry: VoiceChatPeerEntry? var mainEntry: VoiceChatPeerEntry?
for member in callMembers.0 { for member in callMembers.0 {
@ -4786,8 +4795,11 @@ public final class VoiceChatController: ViewController {
} }
var memberPeer = member.peer var memberPeer = member.peer
if member.peer.id == self.callState?.myPeerId, let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar { if member.peer.id == self.callState?.myPeerId {
memberPeer = user.withUpdatedPhoto([photo]) joinedVideo = member.joinedVideo
if let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar {
memberPeer = user.withUpdatedPhoto([photo])
}
} }
if let videoEndpointId = member.videoEndpointId { if let videoEndpointId = member.videoEndpointId {
@ -4929,6 +4941,13 @@ public final class VoiceChatController: ViewController {
} }
} }
if !joinedVideo && !tileItems.isEmpty || !gridTileItems.isEmpty, let peer = self.peer {
tileItems.removeAll()
gridTileItems.removeAll()
tileItems.append(VoiceChatTileItem(account: self.context.account, peer: peer, videoEndpointId: "", videoReady: false, videoTimeouted: true, isVideoLimit: true, isPaused: false, isOwnScreencast: false, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: false, secondary: false, isTablet: false, icon: .none, text: .none, additionalText: nil, action: {}, contextAction: nil, getVideo: { _ in return nil }, getAudioLevel: nil))
}
for member in callMembers.0 { for member in callMembers.0 {
if processedFullscreenPeerIds.contains(member.peer.id) { if processedFullscreenPeerIds.contains(member.peer.id) {
continue continue

View File

@ -358,8 +358,11 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
self.isExtracted = isExtracted self.isExtracted = isExtracted
if item.peer.smallProfileImage != nil { if item.peer.smallProfileImage != nil {
let springDuration: Double = 0.42
let springDamping: CGFloat = 124.0
if isExtracted { if isExtracted {
let profileNode = VoiceChatPeerProfileNode(context: item.context, size: extractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in let profileNode = VoiceChatPeerProfileNode(context: item.context, size: extractedRect.size, sourceSize: nonExtractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in
self?.contextSourceNode.requestDismiss?() self?.contextSourceNode.requestDismiss?()
}) })
profileNode.frame = CGRect(origin: CGPoint(), size: extractedRect.size) profileNode.frame = CGRect(origin: CGPoint(), size: extractedRect.size)
@ -367,6 +370,11 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
self.contextSourceNode.contentNode.addSubnode(profileNode) self.contextSourceNode.contentNode.addSubnode(profileNode)
profileNode.animateIn(from: self, targetRect: extractedRect, transition: transition) profileNode.animateIn(from: self, targetRect: extractedRect, transition: transition)
var appearenceTransition = transition
if transition.isAnimated {
appearenceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0))
}
appearenceTransition.updateFrame(node: profileNode, frame: extractedRect)
self.contextSourceNode.contentNode.customHitTest = { [weak self] point in self.contextSourceNode.contentNode.customHitTest = { [weak self] point in
if let strongSelf = self, let profileNode = strongSelf.profileNode { if let strongSelf = self, let profileNode = strongSelf.profileNode {
@ -377,9 +385,18 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
return nil return nil
} }
self.highlightNode.isHidden = true self.highlightNode.isHidden = true
self.backgroundImageNode.isHidden = true
} else if let profileNode = self.profileNode { } else if let profileNode = self.profileNode {
self.profileNode = nil self.profileNode = nil
profileNode.animateOut(to: self, targetRect: nonExtractedRect, transition: transition) profileNode.animateOut(to: self, targetRect: nonExtractedRect, transition: transition, completion: { [weak self] in
self?.backgroundImageNode.isHidden = false
})
var appearenceTransition = transition
if transition.isAnimated {
appearenceTransition = .animated(duration: 0.2, curve: .easeInOut)
}
appearenceTransition.updateFrame(node: profileNode, frame: nonExtractedRect)
self.contextSourceNode.contentNode.customHitTest = nil self.contextSourceNode.contentNode.customHitTest = nil
self.highlightNode.isHidden = !item.active self.highlightNode.isHidden = !item.active

View File

@ -39,7 +39,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
private var appeared = false private var appeared = false
init(context: AccountContext, size: CGSize, peer: Peer, text: VoiceChatParticipantItem.ParticipantText, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError>, requestDismiss: (() -> Void)?) { init(context: AccountContext, size: CGSize, sourceSize: CGSize, peer: Peer, text: VoiceChatParticipantItem.ParticipantText, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError>, requestDismiss: (() -> Void)?) {
self.context = context self.context = context
self.size = size self.size = size
self.peer = peer self.peer = peer
@ -73,6 +73,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
} }
self.infoNode = ASDisplayNode() self.infoNode = ASDisplayNode()
self.infoNode.clipsToBounds = true
self.titleNode = ImmediateTextNode() self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
@ -122,10 +123,10 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
strongSelf.avatarListNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) strongSelf.avatarListNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
} }
self.updateInfo(size: size, animate: false) self.updateInfo(size: size, sourceSize: sourceSize, animate: false)
} }
func updateInfo(size: CGSize, animate: Bool) { func updateInfo(size: CGSize, sourceSize: CGSize, animate: Bool) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let titleFont = Font.regular(17.0) let titleFont = Font.regular(17.0)
@ -169,7 +170,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
self.statusNode.frame = CGRect(origin: CGPoint(x: 14.0, y: titleSize.height + 3.0), size: statusLayout) self.statusNode.frame = CGRect(origin: CGPoint(x: 14.0, y: titleSize.height + 3.0), size: statusLayout)
let totalHeight = titleSize.height + statusLayout.height + 3.0 + 8.0 let totalHeight = titleSize.height + statusLayout.height + 3.0 + 8.0
let infoFrame = CGRect(x: 0.0, y: size.height - totalHeight, width: self.size.width, height: totalHeight) let infoFrame = CGRect(x: 0.0, y: size.height - totalHeight, width: sourceSize.width, height: totalHeight)
if animate { if animate {
let springDuration: Double = !self.appeared ? 0.42 : 0.3 let springDuration: Double = !self.appeared ? 0.42 : 0.3
@ -195,8 +196,8 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
if let sourceNode = sourceNode as? VoiceChatTileItemNode { if let sourceNode = sourceNode as? VoiceChatTileItemNode {
let sourceRect = sourceNode.bounds let sourceRect = sourceNode.bounds
self.backgroundImageNode.frame = sourceNode.bounds self.backgroundImageNode.frame = sourceNode.bounds
self.updateInfo(size: sourceNode.bounds.size, animate: false) self.updateInfo(size: sourceNode.bounds.size, sourceSize: sourceNode.bounds.size, animate: false)
self.updateInfo(size: targetRect.size, animate: true) self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true)
self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
@ -221,15 +222,15 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0
var appearenceTransition = transition var appearanceTransition = transition
if transition.isAnimated { if transition.isAnimated {
appearenceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0))
} }
if let videoNode = sourceNode.videoNode { if let videoNode = sourceNode.videoNode {
videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearenceTransition) videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition)
appearenceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize))
appearenceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius)))
sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius
} }
self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode)
@ -242,11 +243,11 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode) self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode)
self.view.insertSubview(snapshotView, aboveSubview: sourceNode.videoContainerNode.view) self.view.insertSubview(snapshotView, aboveSubview: sourceNode.videoContainerNode.view)
snapshotView.frame = sourceRect snapshotView.frame = sourceRect
appearenceTransition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - snapshotView.frame.size.height), size: snapshotView.frame.size)) appearanceTransition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - snapshotView.frame.size.height), size: snapshotView.frame.size))
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview() snapshotView.removeFromSuperview()
}) })
appearenceTransition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetSize.width, height: self.videoFadeNode.frame.height))) appearanceTransition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetSize.width, height: self.videoFadeNode.frame.height)))
self.videoFadeNode.alpha = 0.0 self.videoFadeNode.alpha = 0.0
self.videoFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.videoFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
} }
@ -283,9 +284,9 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
} else if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode { } else if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode {
let sourceRect = sourceNode.bounds let sourceRect = sourceNode.bounds
self.backgroundImageNode.frame = sourceNode.bounds self.backgroundImageNode.frame = sourceNode.bounds
self.updateInfo(size: sourceNode.bounds.size, animate: false) self.updateInfo(size: sourceNode.bounds.size, sourceSize: sourceNode.bounds.size, animate: false)
self.updateInfo(size: targetRect.size, animate: true) self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true)
self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds) context.clear(bounds)
@ -298,8 +299,13 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0) transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0)
let initialRect = sourceNode.frame let initialRect: CGRect
let initialScale: CGFloat = sourceRect.width / targetRect.width if let videoNode = sourceNode.videoNode, videoNode.supernode == sourceNode.videoContainerNode, !videoNode.alpha.isZero {
initialRect = sourceRect
} else {
initialRect = sourceNode.avatarNode.frame
}
let initialScale = initialRect.width / targetRect.width
let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width) let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width)
self.avatarListWrapperNode.update(size: targetSize, transition: .immediate) self.avatarListWrapperNode.update(size: targetSize, transition: .immediate)
@ -309,14 +315,30 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0
if false, let videoNode = sourceNode.videoNode { var appearanceTransition = transition
videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: transition) if transition.isAnimated {
transition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0))
transition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) }
if let videoNode = sourceNode.videoNode {
videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition)
appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize))
appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius)))
sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius
} }
// self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode)
let transitionNode = ASImageNode()
transitionNode.clipsToBounds = true
transitionNode.displaysAsynchronously = false
transitionNode.displayWithoutProcessing = true
transitionNode.image = sourceNode.avatarNode.unroundedImage
transitionNode.frame = CGRect(origin: CGPoint(), size: targetSize)
transitionNode.cornerRadius = targetRect.width / 2.0
radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0)
sourceNode.avatarNode.isHidden = true
self.avatarListWrapperNode.contentNode.insertSubnode(transitionNode, at: 0)
// if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) { // if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) {
// self.videoFadeNode.image = sourceNode.fadeNode.image // self.videoFadeNode.image = sourceNode.fadeNode.image
// self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height) // self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height)
@ -337,7 +359,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.avatarListNode.updateCustomItemsOnlySynchronously = false strongSelf.avatarListNode.updateCustomItemsOnlySynchronously = false
// strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode) strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode)
} }
}) })
@ -372,7 +394,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
let springDamping: CGFloat = 1000.0 let springDamping: CGFloat = 1000.0
if let targetNode = targetNode as? VoiceChatTileItemNode { if let targetNode = targetNode as? VoiceChatTileItemNode {
let initialSize = self.bounds let initialSize = self.bounds
self.updateInfo(size: targetRect.size, animate: true) self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true)
transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius)
@ -429,7 +451,9 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
} else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode { } else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode {
let initialSize = self.bounds let initialSize = self.bounds
self.updateInfo(size: targetRect.size, animate: true) self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true)
targetNode.avatarNode.isHidden = false
transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius)

View File

@ -267,7 +267,7 @@ final class VoiceChatTileItemNode: ASDisplayNode {
let springDuration: Double = 0.42 let springDuration: Double = 0.42
let springDamping: CGFloat = 124.0 let springDamping: CGFloat = 124.0
if isExtracted { if isExtracted {
let profileNode = VoiceChatPeerProfileNode(context: self.context, size: extractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in let profileNode = VoiceChatPeerProfileNode(context: self.context, size: extractedRect.size, sourceSize: nonExtractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in
self?.contextSourceNode.requestDismiss?() self?.contextSourceNode.requestDismiss?()
}) })
profileNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) profileNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
@ -411,7 +411,11 @@ final class VoiceChatTileItemNode: ASDisplayNode {
self.videoNode?.updateIsBlurred(isBlurred: item.isPaused, light: true) self.videoNode?.updateIsBlurred(isBlurred: item.isPaused, light: true)
var showPlaceholder = false var showPlaceholder = false
if item.isOwnScreencast { if item.isVideoLimit {
self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_VideoParticipantsLimitExceeded, font: Font.semibold(13.0), textColor: .white)
self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pause"), color: .white)
showPlaceholder = true
} else if item.isOwnScreencast {
self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_YouAreSharingScreen, font: Font.semibold(13.0), textColor: .white) self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_YouAreSharingScreen, font: Font.semibold(13.0), textColor: .white)
self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: item.isTablet ? "Call/ScreenShareTablet" : "Call/ScreenSharePhone"), color: .white) self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: item.isTablet ? "Call/ScreenShareTablet" : "Call/ScreenSharePhone"), color: .white)
showPlaceholder = true showPlaceholder = true
@ -428,7 +432,9 @@ final class VoiceChatTileItemNode: ASDisplayNode {
let titleFont = Font.semibold(13.0) let titleFont = Font.semibold(13.0)
let titleColor = UIColor.white let titleColor = UIColor.white
var titleAttributedString: NSAttributedString? var titleAttributedString: NSAttributedString?
if let user = item.peer as? TelegramUser { if item.isVideoLimit {
titleAttributedString = nil
} else if let user = item.peer as? TelegramUser {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
let string = NSMutableAttributedString() let string = NSMutableAttributedString()
switch item.nameDisplayOrder { switch item.nameDisplayOrder {

View File

@ -123,7 +123,6 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
let parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) } let parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) }
return GroupCallSummary( return GroupCallSummary(
info: info, info: info,
topParticipants: parsedParticipants topParticipants: parsedParticipants
@ -638,6 +637,7 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
videoDescription = nil videoDescription = nil
presentationDescription = nil presentationDescription = nil
} }
let joinedVideo = (flags & (1 << 15)) != 0
if !state.participants.contains(where: { $0.peer.id == peer.id }) { if !state.participants.contains(where: { $0.peer.id == peer.id }) {
state.participants.append(GroupCallParticipantsContext.Participant( state.participants.append(GroupCallParticipantsContext.Participant(
peer: peer, peer: peer,
@ -651,7 +651,8 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
activityRank: nil, activityRank: nil,
muteState: muteState, muteState: muteState,
volume: volume, volume: volume,
about: about about: about,
joinedVideo: joinedVideo
)) ))
} }
} }
@ -869,6 +870,7 @@ public final class GroupCallParticipantsContext {
public var muteState: MuteState? public var muteState: MuteState?
public var volume: Int32? public var volume: Int32?
public var about: String? public var about: String?
public var joinedVideo: Bool
public init( public init(
peer: Peer, peer: Peer,
@ -882,7 +884,8 @@ public final class GroupCallParticipantsContext {
activityRank: Int?, activityRank: Int?,
muteState: MuteState?, muteState: MuteState?,
volume: Int32?, volume: Int32?,
about: String? about: String?,
joinedVideo: Bool
) { ) {
self.peer = peer self.peer = peer
self.ssrc = ssrc self.ssrc = ssrc
@ -896,6 +899,7 @@ public final class GroupCallParticipantsContext {
self.muteState = muteState self.muteState = muteState
self.volume = volume self.volume = volume
self.about = about self.about = about
self.joinedVideo = joinedVideo
} }
public var description: String { public var description: String {
@ -1130,6 +1134,7 @@ public final class GroupCallParticipantsContext {
public var participationStatusChange: ParticipationStatusChange public var participationStatusChange: ParticipationStatusChange
public var volume: Int32? public var volume: Int32?
public var about: String? public var about: String?
public var joinedVideo: Bool
public var isMin: Bool public var isMin: Bool
init( init(
@ -1144,6 +1149,7 @@ public final class GroupCallParticipantsContext {
participationStatusChange: ParticipationStatusChange, participationStatusChange: ParticipationStatusChange,
volume: Int32?, volume: Int32?,
about: String?, about: String?,
joinedVideo: Bool,
isMin: Bool isMin: Bool
) { ) {
self.peerId = peerId self.peerId = peerId
@ -1157,6 +1163,7 @@ public final class GroupCallParticipantsContext {
self.participationStatusChange = participationStatusChange self.participationStatusChange = participationStatusChange
self.volume = volume self.volume = volume
self.about = about self.about = about
self.joinedVideo = joinedVideo
self.isMin = isMin self.isMin = isMin
} }
} }
@ -1686,7 +1693,6 @@ public final class GroupCallParticipantsContext {
volume = previousVolume volume = previousVolume
} }
} }
let participant = Participant( let participant = Participant(
peer: peer, peer: peer,
ssrc: participantUpdate.ssrc, ssrc: participantUpdate.ssrc,
@ -1699,7 +1705,8 @@ public final class GroupCallParticipantsContext {
activityRank: previousActivityRank, activityRank: previousActivityRank,
muteState: muteState, muteState: muteState,
volume: volume, volume: volume,
about: participantUpdate.about about: participantUpdate.about,
joinedVideo: participantUpdate.joinedVideo
) )
updatedParticipants.append(participant) updatedParticipants.append(participant)
} }
@ -2057,6 +2064,7 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate {
} }
let isRemoved = (flags & (1 << 1)) != 0 let isRemoved = (flags & (1 << 1)) != 0
let justJoined = (flags & (1 << 4)) != 0 let justJoined = (flags & (1 << 4)) != 0
let joinedVideo = (flags & (1 << 15)) != 0
let isMin = (flags & (1 << 8)) != 0 let isMin = (flags & (1 << 8)) != 0
let participationStatusChange: GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate.ParticipationStatusChange let participationStatusChange: GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate.ParticipationStatusChange
@ -2086,6 +2094,7 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate {
participationStatusChange: participationStatusChange, participationStatusChange: participationStatusChange,
volume: volume, volume: volume,
about: about, about: about,
joinedVideo: joinedVideo,
isMin: isMin isMin: isMin
) )
} }
@ -2454,6 +2463,8 @@ extension GroupCallParticipantsContext.Participant {
videoDescription = nil videoDescription = nil
presentationDescription = nil presentationDescription = nil
} }
let joinedVideo = (flags & (1 << 15)) != 0
self.init( self.init(
peer: peer, peer: peer,
ssrc: ssrc, ssrc: ssrc,
@ -2466,7 +2477,8 @@ extension GroupCallParticipantsContext.Participant {
activityRank: nil, activityRank: nil,
muteState: muteState, muteState: muteState,
volume: volume, volume: volume,
about: about about: about,
joinedVideo: joinedVideo
) )
} }
} }

View File

@ -11,10 +11,18 @@ public enum ServerProvidedSuggestion: String {
case validatePassword = "VALIDATE_PASSWORD" case validatePassword = "VALIDATE_PASSWORD"
} }
public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProvidedSuggestion], NoError> { private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:])
private var dismissedSuggestions: [AccountRecordId: Set<ServerProvidedSuggestion>] = [:] {
didSet {
dismissedSuggestionsPromise.set(dismissedSuggestions)
}
}
public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProvidedSuggestion], NoError> {
let key: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.appConfiguration])) let key: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.appConfiguration]))
return postbox.combinedView(keys: [key]) return combineLatest(account.postbox.combinedView(keys: [key]), dismissedSuggestionsPromise.get())
|> map { views -> [ServerProvidedSuggestion] in |> map { views, dismissedSuggestionsValue -> [ServerProvidedSuggestion] in
let dismissedSuggestions = dismissedSuggestionsValue[account.id] ?? Set()
guard let view = views.views[key] as? PreferencesView else { guard let view = views.views[key] as? PreferencesView else {
return [] return []
} }
@ -26,12 +34,17 @@ public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProv
} }
return list.compactMap { item -> ServerProvidedSuggestion? in return list.compactMap { item -> ServerProvidedSuggestion? in
return ServerProvidedSuggestion(rawValue: item) return ServerProvidedSuggestion(rawValue: item)
} }.filter { !dismissedSuggestions.contains($0) }
} }
|> distinctUntilChanged |> distinctUntilChanged
} }
public func dismissServerProvidedSuggestion(account: Account, suggestion: ServerProvidedSuggestion) -> Signal<Never, NoError> { public func dismissServerProvidedSuggestion(account: Account, suggestion: ServerProvidedSuggestion) -> Signal<Never, NoError> {
if let _ = dismissedSuggestions[account.id] {
dismissedSuggestions[account.id]?.insert(suggestion)
} else {
dismissedSuggestions[account.id] = Set([suggestion])
}
return account.network.request(Api.functions.help.dismissSuggestion(peer: .inputPeerEmpty, suggestion: suggestion.rawValue)) return account.network.request(Api.functions.help.dismissSuggestion(peer: .inputPeerEmpty, suggestion: suggestion.rawValue))
|> `catch` { _ -> Signal<Api.Bool, NoError> in |> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse) return .single(.boolFalse)

View File

@ -7173,7 +7173,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})) }))
} }
self.checksTooltipDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox) self.checksTooltipDisposable.set((getServerProvidedSuggestions(account: self.context.account)
|> deliverOnMainQueue).start(next: { [weak self] values in |> deliverOnMainQueue).start(next: { [weak self] values in
guard let strongSelf = self else { guard let strongSelf = self else {
return return

View File

@ -0,0 +1,108 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListPeerItem
import SwiftSignalKit
import AccountContext
import Postbox
import SyncCore
import TelegramCore
import ItemListUI
final class PeerInfoScreenInfoItem: PeerInfoScreenItem {
let id: AnyHashable
let title: String
let text: InfoListItemText
let linkAction: ((InfoListItemLinkAction) -> Void)?
init(
id: AnyHashable,
title: String,
text: InfoListItemText,
linkAction: ((InfoListItemLinkAction) -> Void)?
) {
self.id = id
self.title = title
self.text = text
self.linkAction = linkAction
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenInfoItemNode()
}
}
private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode {
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenInfoItem?
private var itemNode: InfoItemNode?
override init() {
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.bottomSeparatorNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenInfoItem else {
return 10.0
}
self.item = item
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), title: item.title, text: item.text, style: .plain, linkAction: { link in
item.linkAction?(link)
}, closeAction: nil)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: InfoItemNode
if let current = self.itemNode {
itemNode = current
infoItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
infoItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! InfoItemNode
self.itemNode = itemNode
self.addSubnode(itemNode)
}
let height = itemNode.contentSize.height
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
var separatorInset: CGFloat = sideInset
if bottomItem != nil {
separatorInset += 49.0
}
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}

View File

@ -397,9 +397,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account
combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks), combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks),
hasPassport, hasPassport,
(context.watchManager?.watchAppInstalled ?? .single(false)), (context.watchManager?.watchAppInstalled ?? .single(false)),
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]),
getServerProvidedSuggestions(account: context.account)
) )
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences -> PeerInfoScreenData in |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions -> PeerInfoScreenData in
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
@ -416,7 +417,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account
enableQRLogin = true enableQRLogin = true
} }
let globalSettings = TelegramGlobalSettings(suggestPhoneNumberConfirmation: false, accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions?.0, webSessionsContext: accountSessions?.2, otherSessionsCount: accountSessions?.1, proxySettings: proxySettings, notificationAuthorizationStatus: notificationsAuthorizationStatus, notificationWarningSuppressed: notificationsWarningSuppressed, notificationExceptions: notificationExceptions, inAppNotificationSettings: inAppNotificationSettings, privacySettings: privacySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin) let globalSettings = TelegramGlobalSettings(suggestPhoneNumberConfirmation: suggestions.contains(.validatePhoneNumber), accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions?.0, webSessionsContext: accountSessions?.2, otherSessionsCount: accountSessions?.1, proxySettings: proxySettings, notificationAuthorizationStatus: notificationsAuthorizationStatus, notificationWarningSuppressed: notificationsWarningSuppressed, notificationExceptions: notificationExceptions, inAppNotificationSettings: inAppNotificationSettings, privacySettings: privacySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin)
return PeerInfoScreenData( return PeerInfoScreenData(
peer: peerView.peers[peerId], peer: peerView.peers[peerId],

View File

@ -568,6 +568,7 @@ private final class PeerInfoInteraction {
let accountContextMenu: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void let accountContextMenu: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void
let updateBio: (String) -> Void let updateBio: (String) -> Void
let openDeletePeer: () -> Void let openDeletePeer: () -> Void
let openFaq: (String?) -> Void
init( init(
openUsername: @escaping (String) -> Void, openUsername: @escaping (String) -> Void,
@ -605,7 +606,8 @@ private final class PeerInfoInteraction {
logoutAccount: @escaping (AccountRecordId) -> Void, logoutAccount: @escaping (AccountRecordId) -> Void,
accountContextMenu: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, accountContextMenu: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void,
updateBio: @escaping (String) -> Void, updateBio: @escaping (String) -> Void,
openDeletePeer: @escaping () -> Void openDeletePeer: @escaping () -> Void,
openFaq: @escaping (String?) -> Void
) { ) {
self.openUsername = openUsername self.openUsername = openUsername
self.openPhone = openPhone self.openPhone = openPhone
@ -643,6 +645,7 @@ private final class PeerInfoInteraction {
self.accountContextMenu = accountContextMenu self.accountContextMenu = accountContextMenu
self.updateBio = updateBio self.updateBio = updateBio
self.openDeletePeer = openDeletePeer self.openDeletePeer = openDeletePeer
self.openFaq = openFaq
} }
} }
@ -692,16 +695,17 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
if let settings = data.globalSettings { if let settings = data.globalSettings {
if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser { if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser {
//
// entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, presentationData.strings.Settings_CheckPhoneNumberText))
// entries.append(.keepPhone(presentationData.theme, presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0))
// entries.append(.changePhone(presentationData.theme, presentationData.strings.Settings_ChangePhoneNumber))
let phoneNumber = formatPhoneNumber(peer.phone ?? "") let phoneNumber = formatPhoneNumber(peer.phone ?? "")
items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0, action: { items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, text: .markdown(presentationData.strings.Settings_CheckPhoneNumberText), linkAction: { link in
interaction.openSettings(.addAccount) if case .tap = link {
interaction.openFaq("q-i-have-a-new-phone-number-what-do-i-do")
}
}))
items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0, action: {
let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePhoneNumber).start()
})) }))
items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_ChangePhoneNumber, action: { items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_ChangePhoneNumber, action: {
interaction.openSettings(.addAccount) interaction.openSettings(.phoneNumber)
})) }))
} }
@ -1683,6 +1687,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, },
openDeletePeer: { [weak self] in openDeletePeer: { [weak self] in
self?.openDeletePeer() self?.openDeletePeer()
},
openFaq: { [weak self] anchor in
self?.openFaq(anchor: anchor)
} }
) )
@ -5421,12 +5428,24 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
private func openFaq(anchor: String? = nil) { private func openFaq(anchor: String? = nil) {
let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) let presentationData = self.presentationData
self.controller?.present(controller, in: .window(.root)) let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.controller?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let _ = (self.cachedFaq.get() let _ = (self.cachedFaq.get()
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] resolvedUrl in |> deliverOnMainQueue).start(next: { [weak self] resolvedUrl in
controller?.dismiss() progressDisposable.dispose()
if let strongSelf = self, let resolvedUrl = resolvedUrl { if let strongSelf = self, let resolvedUrl = resolvedUrl {
var resolvedUrl = resolvedUrl var resolvedUrl = resolvedUrl
@ -6531,14 +6550,15 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings") icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")
} }
let tabBarItem: Signal<(String, UIImage?, UIImage?, String?), NoError> = combineLatest(queue: .mainQueue(), self.context.sharedContext.presentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), accountTabBarAvatar, accountTabBarAvatarBadge) let tabBarItem: Signal<(String, UIImage?, UIImage?, String?), NoError> = combineLatest(queue: .mainQueue(), self.context.sharedContext.presentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), getServerProvidedSuggestions(account: self.context.account), accountTabBarAvatar, accountTabBarAvatarBadge)
|> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, accountTabBarAvatar, accountTabBarAvatarBadge -> (String, UIImage?, UIImage?, String?) in |> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, suggestions, accountTabBarAvatar, accountTabBarAvatarBadge -> (String, UIImage?, UIImage?, String?) in
let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)
let phoneNumberWarning = suggestions.contains(.validatePhoneNumber)
var otherAccountsBadge: String? var otherAccountsBadge: String?
if accountTabBarAvatarBadge > 0 { if accountTabBarAvatarBadge > 0 {
otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} }
return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning ? "!" : otherAccountsBadge) return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning ? "!" : otherAccountsBadge)
} }
self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue in self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue in