Merge commit 'ce883e2d24cc58f728e08c73bb77a1650e99b648'

This commit is contained in:
Ali 2020-11-24 21:11:05 +04:00
commit fe9f5c5014
58 changed files with 7148 additions and 5406 deletions

View File

@ -5895,3 +5895,45 @@ Sorry for the inconvenience.";
"Conversation.EditingPhotoPanelTitle" = "Edit Photo";
"Conversation.TextCopied" = "Text copied to clipboard";
"Media.LimitedAccessTitle" = "Limited Access to Media";
"Media.LimitedAccessText" = "You've given Telegram access only to select number of photos.";
"Media.LimitedAccessManage" = "Manage";
"VoiceChat.BackTitle" = "Chat";
"VoiceChat.StatusSpeaking" = "speaking";
"VoiceChat.StatusListening" = "listening";
"VoiceChat.Connecting" = "Connecting...";
"VoiceChat.Reconnecting" = "Reconnecting...";
"VoiceChat.Unmute" = "Unmute";
"VoiceChat.UnmuteHelp" = "or hold and speak";
"VoiceChat.Live" = "You're Live";
"VoiceChat.Mute" = "Tap to Mute";
"VoiceChat.Muted" = "Muted";
"VoiceChat.MutedHelp" = "You are in Listen Only mode";
"VoiceChat.Audio" = "audio";
"VoiceChat.Leave" = "leave";
"VoiceChat.SpeakPermissionEveryone" = "All Members Can Speak";
"VoiceChat.SpeakPermissionAdmin" = "Only Admins Can Speak";
"VoiceChat.Share" = "Share Invite Link";
"VoiceChat.EndVoiceChat" = "End Voice Chat";
"VoiceChat.CopyInviteLink" = "Copy Invite Link";
"VoiceChat.UnmutePeer" = "Allow to Speak";
"VoiceChat.MutePeer" = "Mute";
"VoiceChat.InvitePeer" = "Invite";
"VoiceChat.RemovePeer" = "Remove";
"VoiceChat.RemovePeerConfirmation" = "Are you sure you want to remove %@ from the group chat?";
"VoiceChat.RemovePeerRemove" = "Remove";
"VoiceChat.UserInvited" = "You invited **%@** to the voice chat";
"Notification.VoiceChatInvitation" = "%1$@ invited %2$@ to the voice chat";
"Notification.VoiceChatInvitationByYou" = "You invited %1$@ to the voice chat";
"Notification.VoiceChatInvitationForYou" = "%1$@ invited you to the voice chat";

View File

@ -199,6 +199,7 @@ public protocol PresentationGroupCall: class {
var state: Signal<PresentationGroupCallState, NoError> { get }
var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get }
var audioLevels: Signal<[(PeerId, Float)], NoError> { get }
var myAudioLevel: Signal<Float, NoError> { get }
func leave() -> Signal<Bool, NoError>

View File

@ -160,47 +160,6 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
f(.default)
})))
}
if canVideoCall {
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_VideoCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
if let contactsController = contactsController {
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: false)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peerId {
context.sharedContext.navigateToCurrentCall()
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer))
} |> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
if let contactsController = contactsController, let peer = peer {
if let current = current {
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: true)
})]), in: .window(.root))
} else {
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}
})
/*let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId))
}
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
if let contactsController = contactsController, let peer = peer, let current = current {
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: true)
})]), in: .window(.root))
}
})*/
}
}
}
f(.default)
})))
}
return items
}
}

View File

@ -14,6 +14,7 @@ public enum DeleteChatPeerAction {
case clearHistory
case clearCache
case clearCacheSuggestion
case removeFromGroup
}
private let avatarFont = avatarPlaceholderFont(size: 26.0)
@ -115,6 +116,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
}
case .clearHistory:
text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
case .removeFromGroup:
text = strings.VoiceChat_RemovePeerConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
default:
break
}

View File

@ -12,10 +12,12 @@ public enum DeviceMetrics: CaseIterable, Equatable {
case iPhone6Plus
case iPhoneX
case iPhoneXSMax
case iPhoneXr
case iPhone12Mini
case iPhone12
case iPhone12ProMax
case iPad
case iPad102Inch
case iPadPro10Inch
case iPadPro11Inch
case iPadPro
@ -30,10 +32,12 @@ public enum DeviceMetrics: CaseIterable, Equatable {
.iPhone6Plus,
.iPhoneX,
.iPhoneXSMax,
.iPhoneXr,
.iPhone12Mini,
.iPhone12,
.iPhone12ProMax,
.iPad,
.iPad102Inch,
.iPadPro10Inch,
.iPadPro11Inch,
.iPadPro,
@ -41,7 +45,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
]
}
public init(screenSize: CGSize, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?) {
public init(screenSize: CGSize, scale: CGFloat, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?) {
var screenSize = screenSize
if screenSize.width > screenSize.height {
screenSize = CGSize(width: screenSize.height, height: screenSize.width)
@ -63,7 +67,11 @@ public enum DeviceMetrics: CaseIterable, Equatable {
let width = device.screenSize.width
let height = device.screenSize.height
if ((screenSize.width.isEqual(to: width) && screenSize.height.isEqual(to: height)) || (additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height))) {
self = device
if case .iPhoneXSMax = device, scale == 2.0 {
self = .iPhoneXr
} else {
self = device
}
return
}
}
@ -72,7 +80,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public var type: DeviceType {
switch self {
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
return .tablet
case let .unknown(screenSize, _, _) where screenSize.width >= 768.0 && screenSize.height >= 1024.0:
return .tablet
@ -93,7 +101,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return CGSize(width: 414.0, height: 736.0)
case .iPhoneX:
return CGSize(width: 375.0, height: 812.0)
case .iPhoneXSMax:
case .iPhoneXSMax, .iPhoneXr:
return CGSize(width: 414.0, height: 896.0)
case .iPhone12Mini:
return CGSize(width: 360.0, height: 780.0)
@ -103,6 +111,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return CGSize(width: 428.0, height: 926.0)
case .iPad:
return CGSize(width: 768.0, height: 1024.0)
case .iPad102Inch:
return CGSize(width: 810.0, height: 1080.0)
case .iPadPro10Inch:
return CGSize(width: 834.0, height: 1112.0)
case .iPadPro11Inch:
@ -114,9 +124,32 @@ public enum DeviceMetrics: CaseIterable, Equatable {
}
}
public var screenCornerRadius: CGFloat {
switch self {
case .iPhoneX, .iPhoneXSMax:
return 39.0
case .iPhoneXr:
return 41.0 + UIScreenPixel
case .iPhone12Mini:
return 44.0
case .iPhone12:
return 47.0 + UIScreenPixel
case .iPhone12ProMax:
return 53.0 + UIScreenPixel
case let .unknown(_, _, onScreenNavigationHeight):
if let _ = onScreenNavigationHeight {
return 39.0
} else {
return 0.0
}
default:
return 0.0
}
}
func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
default:
return UIEdgeInsets.zero
@ -125,7 +158,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return inLandscape ? 21.0 : 34.0
case .iPadPro3rdGen, .iPadPro11Inch:
return 21.0
@ -144,10 +177,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func statusBarHeight(for size: CGSize) -> CGFloat? {
let value = self.statusBarHeight
switch self {
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
if self.type == .tablet {
return value
default:
} else {
if size.width < size.height {
return value
} else {
@ -158,7 +190,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
var statusBarHeight: CGFloat {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return 44.0
case .iPadPro11Inch, .iPadPro3rdGen:
return 24.0
@ -176,9 +208,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 162.0
case .iPhone6, .iPhone6Plus:
return 163.0
case .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return 172.0
case .iPad, .iPadPro10Inch:
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 348.0
case .iPadPro11Inch:
return 368.0
@ -197,9 +229,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 226.0
case .iPhoneX, .iPhone12Mini, .iPhone12:
return 291.0
case .iPhoneXSMax, .iPhone12ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax:
return 302.0
case .iPad, .iPadPro10Inch:
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 263.0
case .iPadPro11Inch:
return 283.0
@ -216,9 +248,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
switch self {
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return 37.0
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
return 50.0
case .unknown:
return 37.0
@ -227,11 +259,11 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self {
case .iPhone4, .iPhone5:
return 37.0
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return 44.0
case .iPhone6Plus:
return 45.0
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
return 50.0
case .unknown:
return 44.0
@ -241,7 +273,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public var hasTopNotch: Bool {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return true
default:
return false

View File

@ -294,7 +294,7 @@ public class Window1 {
self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle
let boundsSize = self.hostView.eventView.bounds.size
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, statusBarHeight: statusBarHost?.statusBarFrame.height ?? defaultStatusBarHeight, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? defaultStatusBarHeight, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
self.statusBarHost = statusBarHost
let statusBarHeight: CGFloat
@ -984,7 +984,7 @@ public class Window1 {
}
if self.deviceMetrics.type == .tablet, let onScreenNavigationHeight = self.hostView.onScreenNavigationHeight, onScreenNavigationHeight != self.deviceMetrics.onScreenNavigationHeight(inLandscape: false, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) {
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, statusBarHeight: statusBarHeight ?? defaultStatusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight)
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? defaultStatusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight)
}
let statusBarWasHidden = self.statusBarHidden

View File

@ -22,7 +22,6 @@ swift_library(
"//submodules/ContextUI:ContextUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AccountContext:AccountContext",
"//submodules/AudioBlob:AudioBlob",
],
visibility = [
"//visibility:public",

View File

@ -15,7 +15,6 @@ import TelegramStringFormatting
import PeerPresenceStatusManager
import ContextUI
import AccountContext
import AudioBlob
private final class ShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
@ -347,9 +346,8 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
let shimmering: ItemListPeerItemShimmering?
let displayDecorations: Bool
let disableInteractiveTransitionIfNecessary: Bool
let audioLevel: Signal<Float, NoError>?
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, audioLevel: Signal<Float, NoError>? = nil) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -382,7 +380,6 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
self.shimmering = shimmering
self.displayDecorations = displayDecorations
self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary
self.audioLevel = audioLevel
}
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) {
@ -455,7 +452,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private let containerNode: ContextControllerSourceNode
private var audioLevelView: VoiceBlobView?
fileprivate let avatarNode: AvatarNode
private let titleNode: TextNode
private let labelNode: TextNode
@ -473,9 +469,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode?
private let audioLevelDisposable = MetaDisposable()
override public var canBeSelected: Bool {
if self.editableControlNode != nil || self.disabledOverlayNode != nil {
return false
@ -556,10 +550,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
}
}
deinit {
self.audioLevelDisposable.dispose()
}
override public func didLoad() {
super.didLoad()
@ -1114,51 +1104,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter))
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
let blobFrame = avatarFrame.insetBy(dx: -12.0, dy: -12.0)
if let audioLevel = item.audioLevel {
strongSelf.audioLevelView?.frame = blobFrame
strongSelf.audioLevelDisposable.set((audioLevel
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
if strongSelf.audioLevelView == nil {
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 0.3,
smallBlobRange: (0, 0),
mediumBlobRange: (0.7, 0.8),
bigBlobRange: (0.8, 0.9)
)
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
let playbackMaskLayer = CAShapeLayer()
playbackMaskLayer.frame = maskRect
playbackMaskLayer.fillRule = .evenOdd
let maskPath = UIBezierPath()
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
maskPath.append(UIBezierPath(rect: maskRect))
playbackMaskLayer.path = maskPath.cgPath
audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(.green)
strongSelf.audioLevelView = audioLevelView
strongSelf.containerNode.view.insertSubview(audioLevelView, at: 0)
audioLevelView.startAnimating()
}
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
}))
} else if let audioLevelView = strongSelf.audioLevelView {
strongSelf.audioLevelView = nil
audioLevelView.removeFromSuperview()
strongSelf.audioLevelDisposable.set(nil)
}
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling {

View File

@ -14,6 +14,4 @@
- (TGMediaAsset *)assetAtIndex:(NSUInteger)index;
- (NSUInteger)indexOfAsset:(TGMediaAsset *)asset;
- (NSSet *)itemsIdentifiers;
@end

View File

@ -64,20 +64,4 @@
return index;
}
- (NSSet *)itemsIdentifiers
{
NSMutableSet *itemsIds = [[NSMutableSet alloc] init];
if (_concreteFetchResult != nil)
{
for (PHAsset *asset in _concreteFetchResult)
[itemsIds addObject:asset.localIdentifier];
}
else if (_assets.count > 0)
{
for (TGMediaAsset *asset in _assets)
[itemsIds addObject:asset.uniqueIdentifier];
}
return itemsIds;
}
@end

View File

@ -1251,8 +1251,8 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
- (void)createNewTextLabel
{
TGPaintSwatch *currentSwatch = _portraitSettingsView.swatch;
TGPaintSwatch *whiteSwatch = [TGPaintSwatch swatchWithColor:[UIColor whiteColor] colorLocation:1.0f brushWeight:currentSwatch.brushWeight];
TGPaintSwatch *blackSwatch = [TGPaintSwatch swatchWithColor:[UIColor blackColor] colorLocation:0.85f brushWeight:currentSwatch.brushWeight];
TGPaintSwatch *whiteSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0xffffff) colorLocation:1.0f brushWeight:currentSwatch.brushWeight];
TGPaintSwatch *blackSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0x000000) colorLocation:0.85f brushWeight:currentSwatch.brushWeight];
[self setCurrentSwatch:_selectedTextStyle == TGPhotoPaintTextEntityStyleOutlined ? blackSwatch : whiteSwatch sender:nil];
CGFloat maxWidth = [self fittedContentSize].width - 26.0f;
@ -1608,16 +1608,16 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
strongSelf->_selectedTextStyle = style;
if (style == TGPhotoPaintTextEntityStyleOutlined && [strongSelf->_portraitSettingsView.swatch.color isEqual:[UIColor whiteColor]])
if (style == TGPhotoPaintTextEntityStyleOutlined && [strongSelf->_portraitSettingsView.swatch.color isEqual:UIColorRGB(0xffffff)])
{
TGPaintSwatch *currentSwatch = strongSelf->_portraitSettingsView.swatch;
TGPaintSwatch *blackSwatch = [TGPaintSwatch swatchWithColor:[UIColor blackColor] colorLocation:0.85f brushWeight:currentSwatch.brushWeight];
TGPaintSwatch *blackSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0x000000) colorLocation:0.85f brushWeight:currentSwatch.brushWeight];
[strongSelf setCurrentSwatch:blackSwatch sender:nil];
}
else if (style != TGPhotoPaintTextEntityStyleOutlined && [strongSelf->_portraitSettingsView.swatch.color isEqual:UIColorRGB(0x000000)])
{
TGPaintSwatch *currentSwatch = strongSelf->_portraitSettingsView.swatch;
TGPaintSwatch *whiteSwatch = [TGPaintSwatch swatchWithColor:[UIColor whiteColor] colorLocation:1.0f brushWeight:currentSwatch.brushWeight];
TGPaintSwatch *whiteSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0xffffff) colorLocation:1.0f brushWeight:currentSwatch.brushWeight];
[strongSelf setCurrentSwatch:whiteSwatch sender:nil];
}

View File

@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 294.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhoneXSMax, .iPhone12ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax:
self.buttonSize = 85.0
self.horizontalSecond = 115.0
self.horizontalThird = 230.0
@ -89,7 +89,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 329.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
self.buttonSize = 81.0
self.horizontalSecond = 106.0
self.horizontalThird = 212.0
@ -155,11 +155,11 @@ public struct PasscodeLayout {
self.titleOffset = 162.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 206.0
case .iPhoneXSMax, .iPhone12ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax:
self.titleOffset = 180.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 226.0
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
self.titleOffset = self.keyboard.topOffset - 120.0
self.subtitleOffset = -2.0
self.inputFieldOffset = self.keyboard.topOffset - 76.0

View File

@ -813,10 +813,10 @@ public final class SemanticStatusNode: ASControlNode {
var animate = false
let timestamp = CACurrentMediaTime()
if let transtionContext = self.transitionContext {
if transtionContext.startTime + transtionContext.duration < timestamp {
if let transitionContext = self.transitionContext {
if transitionContext.startTime + transitionContext.duration < timestamp {
self.transitionContext = nil
transtionContext.completion()
transitionContext.completion()
} else {
animate = true
}

View File

@ -358,7 +358,6 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
}
if name == .night {
colors = colors.filter { $0 != .gray }
defaultColor = PresentationThemeAccentColor(baseColor: .white)
} else {
colors = colors.filter { $0 != .white }
}

View File

@ -283,6 +283,7 @@ public final class ShareController: ViewController {
private var currentAccount: Account
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let forcedTheme: PresentationTheme?
private let externalShare: Bool
private let immediateExternalShare: Bool
@ -302,11 +303,11 @@ public final class ShareController: ViewController {
public var dismissed: ((Bool) -> Void)?
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) {
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, openStats: openStats, shares: shares, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId, forcedTheme: forcedTheme, forcedActionTitle: forcedActionTitle)
}
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, openStats: (() -> Void)? = nil, shares: Int? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) {
self.sharedContext = sharedContext
self.currentContext = currentContext
self.currentAccount = currentContext.account
@ -318,8 +319,12 @@ public final class ShareController: ViewController {
self.immediatePeerId = immediatePeerId
self.openStats = openStats
self.shares = shares
self.forcedTheme = forcedTheme
self.presentationData = self.sharedContext.currentPresentationData.with { $0 }
if let forcedTheme = self.forcedTheme {
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
}
super.init(navigationBarPresentationData: nil)
@ -327,7 +332,7 @@ public final class ShareController: ViewController {
switch subject {
case let .url(text):
self.defaultAction = ShareControllerAction(title: self.presentationData.strings.ShareMenu_CopyShareLink, action: { [weak self] in
self.defaultAction = ShareControllerAction(title: forcedActionTitle ?? self.presentationData.strings.ShareMenu_CopyShareLink, action: { [weak self] in
UIPasteboard.general.string = text
self?.controllerNode.cancel?()
})
@ -441,7 +446,7 @@ public final class ShareController: ViewController {
return
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares)
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, shares: self.shares, forcedTheme: self.forcedTheme)
self.controllerNode.dismiss = { [weak self] shared in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
self?.dismissed?(shared)

View File

@ -29,6 +29,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private let sharedContext: SharedAccountContext
private var context: AccountContext?
private var presentationData: PresentationData
private let forcedTheme: PresentationTheme?
private let externalShare: Bool
private let immediateExternalShare: Bool
private var immediatePeerId: PeerId?
@ -80,9 +81,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private let presetText: String?
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?) {
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, shares: Int?, forcedTheme: PresentationTheme?) {
self.sharedContext = sharedContext
self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.forcedTheme = forcedTheme
self.externalShare = externalShare
self.immediateExternalShare = immediateExternalShare
self.immediatePeerId = immediatePeerId
@ -94,6 +96,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.defaultAction = defaultAction
self.requestLayout = requestLayout
if let forcedTheme = self.forcedTheme {
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
}
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
@ -260,6 +266,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
self.presentationData = presentationData
if let forcedTheme = self.forcedTheme {
self.presentationData = self.presentationData.withUpdated(theme: forcedTheme)
}
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)

View File

@ -47,6 +47,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case phoneNumberRequest
case geoProximityReached(from: PeerId, to: PeerId, distance: Int32)
case groupPhoneCall(callId: Int64, accessHash: Int64, duration: Int32?)
case inviteToGroupPhoneCall(callId: Int64, accessHash: Int64, peerId: PeerId)
public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@ -101,6 +102,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
self = .geoProximityReached(from: PeerId(decoder.decodeInt64ForKey("fromId", orElse: 0)), to: PeerId(decoder.decodeInt64ForKey("toId", orElse: 0)), distance: (decoder.decodeInt32ForKey("dst", orElse: 0)))
case 22:
self = .groupPhoneCall(callId: decoder.decodeInt64ForKey("callId", orElse: 0), accessHash: decoder.decodeInt64ForKey("accessHash", orElse: 0), duration: decoder.decodeOptionalInt32ForKey("duration"))
case 23:
self = .inviteToGroupPhoneCall(callId: decoder.decodeInt64ForKey("callId", orElse: 0), accessHash: decoder.decodeInt64ForKey("accessHash", orElse: 0), peerId: PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0)))
default:
self = .unknown
}
@ -200,6 +203,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "duration")
}
case let .inviteToGroupPhoneCall(callId, accessHash, peerId):
encoder.encodeInt32(23, forKey: "_rawValue")
encoder.encodeInt64(callId, forKey: "callId")
encoder.encodeInt64(accessHash, forKey: "accessHash")
encoder.encodeInt64(peerId.toInt64(), forKey: "peerIdId")
}
}
@ -217,6 +225,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
return [channelId]
case let .geoProximityReached(from, to, _):
return [from, to]
case let .inviteToGroupPhoneCall(_, _, peerId):
return [peerId]
default:
return []
}

View File

@ -27,6 +27,13 @@ swift_library(
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/MergeLists:MergeLists",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/ContextUI:ContextUI",
"//submodules/ShareController:ShareController",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem",
"//submodules/AnimationUI:AnimationUI",
"//submodules/UndoUI:UndoUI",
"//submodules/AudioBlob:AudioBlob",
],
visibility = [
"//visibility:public",

View File

@ -11,12 +11,10 @@ private let labelFont = Font.regular(13.0)
final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
struct Content: Equatable {
enum Appearance: Equatable {
enum Color {
enum Color: Equatable {
case red
case green
case redDimmed
case greenDimmed
case grayDimmed
case custom(UInt32)
}
case blurred(isFilled: Bool)
@ -62,7 +60,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
private let contentNode: ASImageNode
private let overlayHighlightNode: ASImageNode
private var statusNode: SemanticStatusNode?
private let textNode: ImmediateTextNode
let textNode: ImmediateTextNode
private let largeButtonSize: CGFloat = 72.0
@ -198,12 +196,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
fillColor = UIColor(rgb: 0xd92326)
case .green:
fillColor = UIColor(rgb: 0x74db58)
case .redDimmed:
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.3)
case .greenDimmed:
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.3)
case .grayDimmed:
fillColor = UIColor(rgb: 0x1C1C1E)
case let .custom(color):
fillColor = UIColor(rgb: color)
}
}
@ -296,12 +290,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
case .green:
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
case .redDimmed:
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.4).withAlphaComponent(0.2)
case .greenDimmed:
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.4).withAlphaComponent(0.2)
case .grayDimmed:
fillColor = UIColor(rgb: 0x1C1C1E).withAlphaComponent(0.2)
case let .custom(color):
fillColor = UIColor(rgb: color).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
}
}

View File

@ -2022,7 +2022,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
}
}
private final class CallPanGestureRecognizer: UIPanGestureRecognizer {
final class CallPanGestureRecognizer: UIPanGestureRecognizer {
private(set) var firstLocation: CGPoint?
public var shouldBegin: ((CGPoint) -> Bool)?

View File

@ -84,6 +84,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var participantsContextStateDisposable = MetaDisposable()
private var participantsContext: GroupCallParticipantsContext?
private let myAudioLevelPipe = ValuePipe<Float>()
public var myAudioLevel: Signal<Float, NoError> {
return self.myAudioLevelPipe.signal()
}
private var myAudioLevelDisposable = MetaDisposable()
private var audioSessionControl: ManagedAudioSessionControl?
private var audioSessionDisposable: Disposable?
private let audioSessionShouldBeActive = ValuePromise<Bool>(false, ignoreRepeated: true)
@ -268,6 +274,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.checkCallDisposable?.dispose()
self.audioLevelsDisposable.dispose()
self.participantsContextStateDisposable.dispose()
self.myAudioLevelDisposable.dispose()
}
private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) {
@ -374,6 +381,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.audioLevelsPipe.putNext(result)
}
}))
self.myAudioLevelDisposable.set((callContext.myAudioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
guard let strongSelf = self else {
return
}
strongSelf.myAudioLevelPipe.putNext(level)
}))
}
}

View File

@ -0,0 +1,823 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.0)
enum VoiceChatActionButtonState {
enum ActiveState {
case cantSpeak
case muted
case on
}
case connecting
case active(state: ActiveState)
}
private enum VoiceChatActionButtonBackgroundNodeType {
case connecting
case disabled
case blob
}
private protocol VoiceChatActionButtonBackgroundNodeState: NSObjectProtocol {
var isAnimating: Bool { get }
var type: VoiceChatActionButtonBackgroundNodeType { get }
func updateAnimations()
}
private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var isAnimating: Bool {
return true
}
var type: VoiceChatActionButtonBackgroundNodeType {
return .connecting
}
func updateAnimations() {
}
}
private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var isAnimating: Bool {
return false
}
var type: VoiceChatActionButtonBackgroundNodeType {
return .disabled
}
func updateAnimations() {
}
}
private final class Blob {
let size: CGSize
let alpha: CGFloat
let pointsCount: Int
let smoothness: CGFloat
let minRandomness: CGFloat
let maxRandomness: CGFloat
let minSpeed: CGFloat
let maxSpeed: CGFloat
var currentScale: CGFloat = 1.0
var minScale: CGFloat
var maxScale: CGFloat
let scaleSpeed: CGFloat
private var speedLevel: CGFloat = 0.0
private var lastSpeedLevel: CGFloat = 0.0
private var fromPoints: [CGPoint]?
private var toPoints: [CGPoint]?
private var currentPoints: [CGPoint]? {
guard let fromPoints = self.fromPoints, let toPoints = self.toPoints else { return nil }
return fromPoints.enumerated().map { offset, fromPoint in
let toPoint = toPoints[offset]
return CGPoint(x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, y: fromPoint.y + (toPoint.y - fromPoint.y) * transition)
}
}
var currentShape: CGPath?
private var transition: CGFloat = 0 {
didSet {
if let currentPoints = self.currentPoints {
self.currentShape = UIBezierPath.smoothCurve(through: currentPoints, length: size.width, smoothness: smoothness).cgPath
}
}
}
var level: CGFloat = 0.0 {
didSet {
self.currentScale = self.minScale + (self.maxScale - self.minScale) * self.level
}
}
private var transitionArguments: (startTime: Double, duration: Double)?
var loop: Bool = false {
didSet {
if let _ = transitionArguments {
} else {
self.animateToNewShape()
}
}
}
init(
size: CGSize,
alpha: CGFloat,
pointsCount: Int,
minRandomness: CGFloat,
maxRandomness: CGFloat,
minSpeed: CGFloat,
maxSpeed: CGFloat,
minScale: CGFloat,
maxScale: CGFloat,
scaleSpeed: CGFloat
) {
self.size = size
self.alpha = alpha
self.pointsCount = pointsCount
self.minRandomness = minRandomness
self.maxRandomness = maxRandomness
self.minSpeed = minSpeed
self.maxSpeed = maxSpeed
self.minScale = minScale
self.maxScale = maxScale
self.scaleSpeed = scaleSpeed
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
self.currentScale = minScale
self.animateToNewShape()
}
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
self.speedLevel = max(self.speedLevel, newSpeedLevel)
if abs(lastSpeedLevel - newSpeedLevel) > 0.3 {
animateToNewShape()
}
}
private func animateToNewShape() {
if let _ = self.transitionArguments {
self.fromPoints = self.currentPoints
self.toPoints = nil
self.transition = 0.0
self.transitionArguments = nil
}
if self.fromPoints == nil {
self.fromPoints = generateNextBlob(for: self.size)
}
if self.toPoints == nil {
self.toPoints = generateNextBlob(for: self.size)
}
let duration: Double = 1.0 / Double(minSpeed + (maxSpeed - minSpeed) * speedLevel)
self.transitionArguments = (CACurrentMediaTime(), duration)
self.lastSpeedLevel = self.speedLevel
self.speedLevel = 0
self.updateAnimations()
}
func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
// if let (startTime, duration) = self.gradientTransitionArguments, duration > 0.0 {
// if let fromLoop = self.fromLoop {
// if fromLoop {
// self.gradientTransition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
// } else {
// self.gradientTransition = max(0.0, min(1.0, 1.0 - CGFloat((timestamp - startTime) / duration)))
// }
// }
// if self.gradientTransition < 1.0 {
// animate = true
// } else {
// self.gradientTransitionArguments = nil
// }
// }
if let (startTime, duration) = self.transitionArguments, duration > 0.0 {
self.transition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
if self.transition < 1.0 {
animate = true
} else {
if self.loop {
self.animateToNewShape()
} else {
self.fromPoints = self.currentPoints
self.toPoints = nil
self.transition = 0.0
self.transitionArguments = nil
}
}
}
// let gradientMovementStartTime: Double
// let gradientMovementDuration: Double
// let gradientMovementReverse: Bool
// if let (startTime, duration, reverse) = self.gradientMovementTransitionArguments, duration > 0.0 {
// gradientMovementStartTime = startTime
// gradientMovementDuration = duration
// gradientMovementReverse = reverse
// } else {
// gradientMovementStartTime = CACurrentMediaTime()
// gradientMovementDuration = 1.0
// gradientMovementReverse = false
// self.gradientMovementTransitionArguments = (gradientMovementStartTime, gradientMovementStartTime, gradientMovementReverse)
// }
// let movementT = CGFloat((timestamp - gradientMovementStartTime) / gradientMovementDuration)
// self.gradientMovementTransition = gradientMovementReverse ? 1.0 - movementT : movementT
// if gradientMovementReverse && self.gradientMovementTransition <= 0.0 {
// self.gradientMovementTransitionArguments = (CACurrentMediaTime(), 1.0, false)
// } else if !gradientMovementReverse && self.gradientMovementTransition >= 1.0 {
// self.gradientMovementTransitionArguments = (CACurrentMediaTime(), 1.0, true)
// }
}
private func generateNextBlob(for size: CGSize) -> [CGPoint] {
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
return blob(pointsCount: pointsCount, randomness: randomness).map {
return CGPoint(x: size.width / 2.0 + $0.x * CGFloat(size.width), y: size.height / 2.0 + $0.y * CGFloat(size.height))
}
}
private func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
let rgen = { () -> CGFloat in
let accuracy: UInt32 = 1000
let random = arc4random_uniform(accuracy)
return CGFloat(random) / CGFloat(accuracy)
}
let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100)
let points = (0 ..< pointsCount).map { i -> CGPoint in
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
let angleRandomness: CGFloat = angle * 0.1
let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5)
let pointX = sin(startAngle + CGFloat(i) * randAngle)
let pointY = cos(startAngle + CGFloat(i) * randAngle)
return CGPoint(x: pointX * randPointOffset, y: pointY * randPointOffset)
}
return points
}
}
private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var isAnimating: Bool {
return true
}
var type: VoiceChatActionButtonBackgroundNodeType {
return .blob
}
typealias BlobRange = (min: CGFloat, max: CGFloat)
let blobs: [Blob]
var active: Bool
var activeTransitionArguments: (startTime: Double, duration: Double)?
init(size: CGSize, active: Bool) {
self.active = active
let mediumBlobRange: BlobRange = (0.69, 0.87)
let bigBlobRange: BlobRange = (0.71, 1.00)
let mediumBlob = Blob(size: size, alpha: 0.55, pointsCount: 8, minRandomness: 1, maxRandomness: 1, minSpeed: 1.5, maxSpeed: 7, minScale: mediumBlobRange.min, maxScale: mediumBlobRange.max, scaleSpeed: 0.2)
let largeBlob = Blob(size: size, alpha: 0.35, pointsCount: 8, minRandomness: 1, maxRandomness: 1, minSpeed: 1.5, maxSpeed: 7, minScale: bigBlobRange.min, maxScale: bigBlobRange.max, scaleSpeed: 0.2)
self.blobs = [largeBlob, mediumBlob]
}
func update(with state: VoiceChatActionButtonBackgroundNodeBlobState) {
if self.active != state.active {
self.active = state.active
self.activeTransitionArguments = (CACurrentMediaTime(), 0.3)
}
}
func updateAnimations() {
for blob in self.blobs {
blob.updateAnimations()
}
}
}
private final class VoiceChatActionButtonBackgroundNodeTransition {
let startTime: Double
let duration: Double
let previousState: VoiceChatActionButtonBackgroundNodeState?
init(startTime: Double, duration: Double, previousState: VoiceChatActionButtonBackgroundNodeState?) {
self.startTime = startTime
self.duration = duration
self.previousState = previousState
}
func progress(time: Double) -> CGFloat {
if duration > 0.0 {
return CGFloat(max(0.0, min(1.0, (time - startTime) / duration)))
} else {
return 0.0
}
}
}
private class VoiceChatActionButtonBackgroundNodeDrawingState: NSObject {
let timestamp: Double
let state: VoiceChatActionButtonBackgroundNodeState
let transition: VoiceChatActionButtonBackgroundNodeTransition?
init(timestamp: Double, state: VoiceChatActionButtonBackgroundNodeState, transition: VoiceChatActionButtonBackgroundNodeTransition?) {
self.timestamp = timestamp
self.state = state
self.transition = transition
}
}
private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
private var state: VoiceChatActionButtonBackgroundNodeState
private var hasState = false
private var transition: VoiceChatActionButtonBackgroundNodeTransition?
var audioLevel: CGFloat = 0.0 {
didSet {
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState {
for blob in blobsState.blobs {
blob.loop = audioLevel.isZero
blob.updateSpeedLevel(to: self.audioLevel)
}
}
}
}
private var presentationAudioLevel: CGFloat = 0.0
private var animator: ConstantDisplayLinkAnimator?
override init() {
self.state = VoiceChatActionButtonBackgroundNodeConnectingState()
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
}
func update(state: VoiceChatActionButtonBackgroundNodeState, animated: Bool) {
var animated = animated
if !self.hasState {
self.hasState = true
animated = false
}
if state.type != self.state.type {
if animated {
self.transition = VoiceChatActionButtonBackgroundNodeTransition(startTime: CACurrentMediaTime(), duration: 0.3, previousState: self.state)
}
self.state = state
} else if let blobState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState, let nextState = state as? VoiceChatActionButtonBackgroundNodeBlobState {
blobState.update(with: nextState)
}
self.updateAnimations()
}
private func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
self.presentationAudioLevel = self.presentationAudioLevel * 0.9 + max(0.1, self.audioLevel) * 0.1
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState {
for blob in blobsState.blobs {
blob.level = self.presentationAudioLevel
}
}
if let transition = self.transition {
if transition.startTime + transition.duration < timestamp {
self.transition = nil
} else {
animate = true
}
}
if self.state.isAnimating {
animate = true
self.state.updateAnimations()
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations()
})
self.animator = animator
}
animator.isPaused = false
} else {
self.animator?.isPaused = true
}
self.setNeedsDisplay()
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return VoiceChatActionButtonBackgroundNodeDrawingState(timestamp: CACurrentMediaTime(), state: self.state, transition: self.transition)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? VoiceChatActionButtonBackgroundNodeDrawingState else {
return
}
let greyColor = UIColor(rgb: 0x1c1c1e)
let buttonSize = CGSize(width: 144.0, height: 144.0)
let radius = buttonSize.width / 2.0
let blue = UIColor(rgb: 0x0078ff)
let lightBlue = UIColor(rgb: 0x59c7f8)
let green = UIColor(rgb: 0x33c659)
var firstColor = lightBlue
var secondColor = blue
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
var gradientCenter = CGPoint(x: bounds.size.width - 30.0, y: 50.0)
let gradientStartRadius: CGFloat = 0.0
let gradientEndRadius: CGFloat = 260.0
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
var gradientTransition: CGFloat = blobsState.active ? 1.0 : 0.0
if let transition = blobsState.activeTransitionArguments {
gradientTransition = CGFloat((parameters.timestamp - transition.startTime) / transition.duration)
if !blobsState.active {
gradientTransition = 1.0 - gradientTransition
}
}
firstColor = firstColor.interpolateTo(blue, fraction: gradientTransition)!
secondColor = secondColor.interpolateTo(green, fraction: gradientTransition)!
let maskGradientStartRadius: CGFloat = 0.0
var maskGradientEndRadius: CGFloat = bounds.size.width / 2.0
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState {
maskGradientEndRadius *= transition.progress(time: parameters.timestamp)
}
let maskGradientCenter = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
let colors: [CGColor] = [secondColor.withAlphaComponent(0.5).cgColor, secondColor.withAlphaComponent(0.0).cgColor]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawRadialGradient(gradient, startCenter: maskGradientCenter, startRadius: maskGradientStartRadius, endCenter: maskGradientCenter, endRadius: maskGradientEndRadius, options: .drawsAfterEndLocation)
// context.setBlendMode(.clear)
//
//
// let maskColors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor]
// let maskGradient = CGGradient(colorsSpace: colorSpace, colors: maskColors as CFArray, locations: &locations)!
//
// let maskGradientStartRadius: CGFloat = 0.0
// let maskGradientEndRadius: CGFloat = bounds.size.width / 2.0
//// context.drawRadialGradient(maskGradient, startCenter: maskGradientCenter, startRadius: maskGradientStartRadius, endCenter: maskGradientCenter, endRadius: maskGradientEndRadius, options: .drawsAfterEndLocation)
//
// context.setBlendMode(.normal)
}
let colors: [CGColor] = [firstColor.cgColor, secondColor.cgColor]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
// center.x -= parameters.gradientMovement * 60.0
// center.y += parameters.gradientMovement * 200.0
context.saveGState()
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
for blob in blobsState.blobs {
if let path = blob.currentShape {
let uiPath = UIBezierPath(cgPath: path)
let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
uiPath.apply(toOrigin)
uiPath.apply(CGAffineTransform(scaleX: blob.currentScale, y: blob.currentScale))
uiPath.apply(fromOrigin)
context.addPath(uiPath.cgPath)
context.clip()
context.setAlpha(blob.alpha)
context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation)
}
}
}
context.restoreGState()
context.setFillColor(greyColor.cgColor)
let buttonRect = bounds.insetBy(dx: (bounds.width - 144.0) / 2.0, dy: (bounds.height - 144.0) / 2.0)
context.fillEllipse(in: buttonRect)
var drawGradient = false
let lineWidth = 3.0 + UIScreenPixel
if parameters.state is VoiceChatActionButtonBackgroundNodeConnectingState || parameters.transition?.previousState is VoiceChatActionButtonBackgroundNodeConnectingState {
var globalAngle: CGFloat = CGFloat(parameters.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0))
globalAngle *= 4.0
globalAngle = CGFloat(globalAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
var timestamp = parameters.timestamp
if let transition = parameters.transition {
timestamp = transition.startTime
}
var skip = false
var progress = CGFloat(1.0 + timestamp.remainder(dividingBy: 2.0))
if let transition = parameters.transition {
var transitionProgress = transition.progress(time: parameters.timestamp)
if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState {
transitionProgress = min(1.0, transitionProgress / 0.5)
progress = progress + (2.0 - progress) * transitionProgress
if transitionProgress >= 1.0 {
skip = true
}
} else if parameters.state is VoiceChatActionButtonBackgroundNodeDisabledState {
progress = progress + (1.0 - progress) * transition.progress(time: parameters.timestamp)
if transitionProgress >= 1.0 {
skip = true
}
}
}
if !skip {
var startAngle = -CGFloat.pi / 2.0 + globalAngle
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
let tmp = startAngle
startAngle = endAngle
endAngle = 2.0 * CGFloat.pi + tmp
}
let path = CGMutablePath()
path.addArc(center: CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let filledPath = path.copy(strokingWithWidth: lineWidth, lineCap: .round, lineJoin: .miter, miterLimit: 10)
context.addPath(filledPath)
context.clip()
drawGradient = true
}
}
var clearInside: CGFloat?
if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState {
let path = CGMutablePath()
path.addEllipse(in: buttonRect.insetBy(dx: -lineWidth / 2.0, dy: -lineWidth / 2.0))
context.addPath(path)
context.clip()
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState || transition.previousState is VoiceChatActionButtonBackgroundNodeDisabledState, transition.progress(time: parameters.timestamp) > 0.5 {
let progress = (transition.progress(time: parameters.timestamp) - 0.5) / 0.5
clearInside = progress
}
drawGradient = true
}
if drawGradient {
context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: gradientStartRadius, endCenter: gradientCenter, endRadius: gradientEndRadius, options: .drawsAfterEndLocation)
}
if let clearInside = clearInside {
context.setFillColor(greyColor.cgColor)
context.fillEllipse(in: buttonRect.insetBy(dx: clearInside * radius, dy: clearInside * radius))
}
}
}
final class VoiceChatActionButton: HighlightTrackingButtonNode {
private let containerNode: ASDisplayNode
private let backgroundNode: VoiceChatActionButtonBackgroundNode
let iconNode: VoiceChatMicrophoneNode
let titleLabel: ImmediateTextNode
let subtitleLabel: ImmediateTextNode
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButtonState, title: String, subtitle: String)?
var pressing: Bool = false {
didSet {
if self.pressing {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: self.containerNode, scale: 0.9)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: self.containerNode, scale: 1.0)
}
}
}
init() {
self.containerNode = ASDisplayNode()
self.backgroundNode = VoiceChatActionButtonBackgroundNode()
self.iconNode = VoiceChatMicrophoneNode()
self.titleLabel = ImmediateTextNode()
self.subtitleLabel = ImmediateTextNode()
super.init()
self.addSubnode(self.titleLabel)
self.addSubnode(self.subtitleLabel)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: strongSelf.containerNode, scale: 0.9)
} else if !strongSelf.pressing {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: strongSelf.containerNode, scale: 1.0)
}
}
}
}
func updateLevel(_ level: CGFloat) {
let maxLevel: CGFloat = 1.0
let normalizedLevel = min(1, max(level / maxLevel, 0))
self.backgroundNode.audioLevel = normalizedLevel
}
func update(size: CGSize, buttonSize: CGSize, state: VoiceChatActionButtonState, title: String, subtitle: String, animated: Bool = false) {
let updatedTitle = self.currentParams?.title != title
let updatedSubtitle = self.currentParams?.subtitle != subtitle
self.currentParams = (size, buttonSize, state, title, subtitle)
self.titleLabel.attributedText = NSAttributedString(string: title, font: titleFont, textColor: .white)
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: subtitleFont, textColor: .white)
var iconMuted = true
var iconColor: UIColor = .white
var backgroundState: VoiceChatActionButtonBackgroundNodeState
switch state {
case let .active(state):
switch state {
case .on:
iconMuted = false
backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: size, active: true)
case .muted:
backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: size, active: false)
case .cantSpeak:
iconColor = UIColor(rgb: 0xff3b30)
backgroundState = VoiceChatActionButtonBackgroundNodeDisabledState()
default:
break
}
case .connecting:
backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState()
}
self.backgroundNode.update(state: backgroundState, animated: true)
if animated {
if let snapshotView = self.titleLabel.view.snapshotContentTree(), updatedTitle {
self.titleLabel.view.superview?.insertSubview(snapshotView, belowSubview: self.titleLabel.view)
snapshotView.frame = self.titleLabel.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let snapshotView = self.subtitleLabel.view.snapshotContentTree(), updatedSubtitle {
self.subtitleLabel.view.superview?.insertSubview(snapshotView, belowSubview: self.subtitleLabel.view)
snapshotView.frame = self.subtitleLabel.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
let titleSize = self.titleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude))
let totalHeight = titleSize.height + subtitleSize.height + 1.0
self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height + 16.0 - totalHeight / 2.0) - 20.0), size: titleSize)
self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
let iconSize = CGSize(width: 90.0, height: 90.0)
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
self.iconNode.update(state: VoiceChatMicrophoneNode.State(muted: iconMuted, color: iconColor), animated: true)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var hitRect = self.bounds
if let (_, buttonSize, _, _, _) = self.currentParams {
hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0)
}
let result = super.hitTest(point, with: event)
if !hitRect.contains(point) {
return nil
}
return result
}
}
private extension UIBezierPath {
static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat) -> UIBezierPath {
var smoothPoints = [SmoothPoint]()
for index in (0 ..< points.count) {
let prevIndex = index - 1
let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex]
let curr = points[index]
let next = points[(index + 1) % points.count]
let angle: CGFloat = {
let dx = next.x - prev.x
let dy = -next.y + prev.y
let angle = atan2(dy, dx)
if angle < 0 {
return abs(angle)
} else {
return 2 * .pi - angle
}
}()
smoothPoints.append(
SmoothPoint(
point: curr,
inAngle: angle + .pi,
inLength: smoothness * distance(from: curr, to: prev),
outAngle: angle,
outLength: smoothness * distance(from: curr, to: next)
)
)
}
let resultPath = UIBezierPath()
resultPath.move(to: smoothPoints[0].point)
for index in (0 ..< smoothPoints.count) {
let curr = smoothPoints[index]
let next = smoothPoints[(index + 1) % points.count]
let currSmoothOut = curr.smoothOut()
let nextSmoothIn = next.smoothIn()
resultPath.addCurve(to: next.point, controlPoint1: currSmoothOut, controlPoint2: nextSmoothIn)
}
resultPath.close()
return resultPath
}
static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat {
return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y))
}
struct SmoothPoint {
let point: CGPoint
let inAngle: CGFloat
let inLength: CGFloat
let outAngle: CGFloat
let outLength: CGFloat
func smoothIn() -> CGPoint {
return smooth(angle: inAngle, length: inLength)
}
func smoothOut() -> CGPoint {
return smooth(angle: outAngle, length: outLength)
}
private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint {
return CGPoint(
x: point.x + length * cos(angle),
y: point.y + length * sin(angle)
)
}
}
}

View File

@ -11,11 +11,13 @@ import AccountContext
import Postbox
import TelegramCore
import SyncCore
import ItemListPeerItem
import MergeLists
import ItemListUI
import AppBundle
import RadialStatusNode
import ContextUI
import ShareController
import DeleteChatPeerActionSheetItem
import UndoUI
private final class VoiceChatControllerTitleView: UIView {
private var theme: PresentationTheme
@ -80,41 +82,6 @@ private final class VoiceChatControllerTitleView: UIView {
}
}
private final class VoiceChatActionButton: HighlightTrackingButtonNode {
private let backgroundNode: ASImageNode
private let foregroundNode: ASImageNode
private var validSize: CGSize?
private var isOn: Bool?
init() {
self.backgroundNode = ASImageNode()
self.foregroundNode = ASImageNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.foregroundNode)
}
func updateLayout(size: CGSize, isOn: Bool) {
if self.validSize != size {
self.validSize = size
self.backgroundNode.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1C1C1E))
}
if self.isOn != isOn {
self.isOn = isOn
self.foregroundNode.image = UIImage(bundleImageName: isOn ? "Call/VoiceChatMicOn" : "Call/VoiceChatMicOff")
}
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
if let image = self.foregroundNode.image {
self.foregroundNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
}
}
public final class VoiceChatController: ViewController {
private final class Node: ViewControllerTracingNode {
private struct ListTransition {
@ -124,17 +91,24 @@ public final class VoiceChatController: ViewController {
let isLoading: Bool
let isEmpty: Bool
let crossFade: Bool
let count: Int
}
private final class Interaction {
let updateIsMuted: (PeerId, Bool) -> Void
let invitePeer: (Peer) -> Void
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
let updateIsMuted: (PeerId, Bool) -> Void
init(
updateIsMuted: @escaping (PeerId, Bool) -> Void
updateIsMuted: @escaping (PeerId, Bool) -> Void,
invitePeer: @escaping (Peer) -> Void,
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
) {
self.updateIsMuted = updateIsMuted
self.invitePeer = invitePeer
self.peerContextAction = peerContextAction
}
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError>? {
@ -147,7 +121,7 @@ public final class VoiceChatController: ViewController {
}
}
func updateAudioLevels(levels: [(PeerId, Float)]) {
func updateAudioLevels(_ levels: [(PeerId, Float)]) {
for (peerId, level) in levels {
if let pipe = self.audioLevels[peerId] {
pipe.putNext(level)
@ -163,70 +137,85 @@ public final class VoiceChatController: ViewController {
case speaking
}
var participant: RenderedChannelParticipant
var peer: Peer
var presence: TelegramUserPresence?
var activityTimestamp: Int32
var state: State
var muteState: GroupCallParticipantsContext.Participant.MuteState?
var stableId: PeerId {
return self.participant.peer.id
return self.peer.id
}
static func ==(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
if !lhs.peer.isEqual(rhs.peer) {
return false
}
if lhs.presence != rhs.presence {
return false
}
if lhs.activityTimestamp != rhs.activityTimestamp {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.muteState != rhs.muteState {
return false
}
return true
}
static func <(lhs: PeerEntry, rhs: PeerEntry) -> Bool {
if lhs.activityTimestamp != rhs.activityTimestamp {
return lhs.activityTimestamp > rhs.activityTimestamp
}
return lhs.participant.peer.id < rhs.participant.peer.id
return lhs.peer.id < rhs.peer.id
}
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListViewItem {
let peer = self.participant.peer
func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem {
let peer = self.peer
let text: ItemListPeerItemText
let text: VoiceChatParticipantItem.ParticipantText
let icon: VoiceChatParticipantItem.Icon
switch self.state {
case .inactive:
text = .presence
icon = .invite
case .listening:
//TODO:localize
let muteString: String
if self.muteState != nil {
muteString = " [muted]"
text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
let microphoneColor: UIColor
if let muteState = self.muteState {
if muteState.canUnmute {
microphoneColor = UIColor(rgb: 0x979797)
} else {
microphoneColor = UIColor(rgb: 0xff3b30)
}
} else {
muteString = ""
microphoneColor = UIColor(rgb: 0x979797)
}
text = .text("listening\(muteString)", .accent)
icon = .microphone(self.muteState != nil, microphoneColor)
case .speaking:
//TODO:localize
text = .text("speaking", .constructive)
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
icon = .microphone(false, UIColor(rgb: 0x34c759))
}
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: context, peer: peer, height: .peerList, presence: self.participant.presences[self.participant.peer.id], text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
//arguments.deleteIncludePeer(peer.peerId)
})]), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
switch self.state {
case .inactive:
break
default:
if self.participant.peer.id != context.account.peerId {
interaction.updateIsMuted(self.participant.peer.id, self.muteState != nil ? false : true)
}
}
}, setPeerIdWithRevealedOptions: { lhs, rhs in
//arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
}, removePeer: { id in
//arguments.deleteIncludePeer(id)
}, noInsets: true, audioLevel: peer.id == context.account.peerId ? nil : interaction.getAudioLevel(peer.id))
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: self.presence, text: text, icon: icon, enabled: true, audioLevel: interaction.getAudioLevel(peer.id), action: {
interaction.invitePeer(peer)
}, contextAction: { node, gesture in
interaction.peerContextAction(self, node, gesture)
})
}
}
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: ItemListPresentationData, interaction: Interaction) -> ListTransition {
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade)
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade, count: toEntries.count)
}
private weak var controller: VoiceChatController?
@ -236,15 +225,15 @@ public final class VoiceChatController: ViewController {
private var presentationData: PresentationData
private var darkTheme: PresentationTheme
private let optionsButton: VoiceChatOptionsButton
private let contentContainer: ASDisplayNode
private let listNode: ListView
private let audioOutputNode: CallControllerButtonItemNode
private let leaveNode: CallControllerButtonItemNode
private let actionButton: VoiceChatActionButton
private let radialStatus: RadialStatusNode
private let statusLabel: ImmediateTextNode
private var enqueuedTransitions: [ListTransition] = []
private var maxListHeight: CGFloat?
private var validLayout: (ContainerViewLayout, CGFloat)?
private var didSetContentsReady: Bool = false
@ -262,12 +251,15 @@ public final class VoiceChatController: ViewController {
private var isMutedDisposable: Disposable?
private var callStateDisposable: Disposable?
private var pushingToTalk = false
private var callState: PresentationGroupCallState?
private var audioOutputStateDisposable: Disposable?
private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)?
private var audioLevelsDisposable: Disposable?
private var myAudioLevelDisposable: Disposable?
private var memberStatesDisposable: Disposable?
private var itemInteraction: Interaction?
@ -279,9 +271,12 @@ public final class VoiceChatController: ViewController {
self.call = call
self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.darkTheme = defaultDarkPresentationTheme
self.darkTheme = defaultDarkColorPresentationTheme
self.optionsButton = VoiceChatOptionsButton()
self.contentContainer = ASDisplayNode()
self.contentContainer.backgroundColor = .black
self.listNode = ListView()
self.listNode.backgroundColor = self.darkTheme.list.itemBlocksBackgroundColor
@ -292,24 +287,117 @@ public final class VoiceChatController: ViewController {
self.audioOutputNode = CallControllerButtonItemNode()
self.leaveNode = CallControllerButtonItemNode()
self.actionButton = VoiceChatActionButton()
self.statusLabel = ImmediateTextNode()
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
super.init()
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
})
let invitePeer: (Peer) -> Void = { [weak self] peer in
guard let strongSelf = self else {
return
}
strongSelf.controller?.present(
UndoOverlayController(
presentationData: strongSelf.presentationData,
content: .invitedToVoiceChat(
context: strongSelf.context,
peer: peer,
text: strongSelf.presentationData.strings.VoiceChat_UserInvited(peer.compactDisplayTitle).0
),
elevatedLayout: false,
action: { action in
return true
}
),
in: .current
)
}
self.backgroundColor = .black
self.itemInteraction = Interaction(
updateIsMuted: { [weak self] peerId, isMuted in
self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
}, invitePeer: { peer in
invitePeer(peer)
}, peerContextAction: { [weak self] entry, sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
return
}
let peer = entry.peer
var items: [ContextMenuItem] = []
switch entry.state {
case .inactive:
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_InvitePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
invitePeer(peer)
f(.default)
})))
default:
if entry.muteState == nil {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false)
f(.default)
})))
}
if peer.id != strongSelf.context.account.peerId {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme))
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: peer, chatPeer: peer, action: .removeFromGroup, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
})))
}
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(items), reactionItems: [], gesture: gesture)
strongSelf.controller?.presentInGlobalOverlay(contextController)
})
self.contentContainer.addSubnode(self.listNode)
self.contentContainer.addSubnode(self.audioOutputNode)
self.contentContainer.addSubnode(self.leaveNode)
self.contentContainer.addSubnode(self.actionButton)
self.contentContainer.addSubnode(self.statusLabel)
self.contentContainer.addSubnode(self.radialStatus)
self.addSubnode(self.contentContainer)
@ -406,7 +494,20 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else {
return
}
strongSelf.itemInteraction?.updateAudioLevels(levels: levels)
strongSelf.itemInteraction?.updateAudioLevels(levels)
})
self.myAudioLevelDisposable = (call.myAudioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
guard let strongSelf = self else {
return
}
var effectiveLevel: Float = 0.0
if let state = strongSelf.callState, !state.isMuted {
effectiveLevel = level
}
strongSelf.itemInteraction?.updateAudioLevels([(strongSelf.context.account.peerId, effectiveLevel)])
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
})
self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
@ -414,6 +515,48 @@ public final class VoiceChatController: ViewController {
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside)
self.optionsButton.contextAction = { [weak self, weak optionsButton] sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller, let strongOptionsButton = optionsButton else {
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { _ in return nil}, action: { _, f in
f(.dismissWithoutContent)
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
if let strongSelf = self {
let shareController = ShareController(context: strongSelf.context, subject: .url("url"), forcedTheme: strongSelf.darkTheme, forcedActionTitle: strongSelf.presentationData.strings.VoiceChat_CopyInviteLink)
strongSelf.controller?.present(shareController, in: .window(.root))
}
})))
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
})))
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: strongOptionsButton.extractedContainerNode, keepInPlace: true)), items: .single(items), reactionItems: [], gesture: gesture)
strongSelf.controller?.presentInGlobalOverlay(contextController)
}
let optionsButtonItem = UIBarButtonItem(customDisplayNode: self.optionsButton)!
optionsButtonItem.target = self
optionsButtonItem.action = #selector(self.rightNavigationButtonAction)
self.controller?.navigationItem.setRightBarButton(optionsButtonItem, animated: false)
}
deinit {
@ -425,6 +568,28 @@ public final class VoiceChatController: ViewController {
self.audioOutputStateDisposable?.dispose()
self.memberStatesDisposable?.dispose()
self.audioLevelsDisposable?.dispose()
self.myAudioLevelDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
longTapRecognizer.minimumPressDuration = 0.3
self.actionButton.view.addGestureRecognizer(longTapRecognizer)
let panRecognizer = CallPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.shouldBegin = { [weak self] _ in
guard let strongSelf = self else {
return false
}
return true
}
self.view.addGestureRecognizer(panRecognizer)
}
@objc private func rightNavigationButtonAction() {
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
}
@objc private func leavePressed() {
@ -434,6 +599,21 @@ public final class VoiceChatController: ViewController {
}))
}
@objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
self.pushingToTalk = true
self.actionButton.pressing = true
self.call.setIsMuted(false)
case .ended, .cancelled:
self.pushingToTalk = false
self.actionButton.pressing = false
self.call.setIsMuted(true)
default:
break
}
}
@objc private func actionButtonPressed() {
self.call.toggleIsMuted()
}
@ -490,13 +670,6 @@ public final class VoiceChatController: ViewController {
}))
}
if hasMute {
items.append(CallRouteActionSheetItem(title: self.presentationData.strings.Call_AudioRouteMute, icon: generateScaledImage(image: UIImage(bundleImageName: "Call/CallMuteButton"), size: CGSize(width: 48.0, height: 48.0), opaque: false), selected: self.callState?.isMuted ?? true, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.call.toggleIsMuted()
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
@ -513,11 +686,16 @@ public final class VoiceChatController: ViewController {
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
let bottomAreaHeight: CGFloat = 302.0
let bottomAreaHeight: CGFloat = 333.0
let listOrigin = CGPoint(x: 16.0, y: navigationHeight + 10.0)
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y)))
var listHeight: CGFloat = 56.0
if let maxListHeight = self.maxListHeight {
listHeight = min(max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y), maxListHeight)
}
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: listHeight))
transition.updateFrame(node: self.listNode, frame: listFrame)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
@ -526,9 +704,48 @@ public final class VoiceChatController: ViewController {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let sideButtonSize = CGSize(width: 60.0, height: 60.0)
let centralButtonSize = CGSize(width: 144.0, height: 144.0)
let centralButtonSize = CGSize(width: 244.0, height: 244.0)
let sideButtonInset: CGFloat = 27.0
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
var isMicOn = false
let actionButtonState: VoiceChatActionButtonState
let actionButtonTitle: String
let actionButtonSubtitle: String
let audioButtonAppearance: CallControllerButtonItemNode.Content.Appearance
if let callState = callState {
isMicOn = !callState.isMuted
switch callState.networkState {
case .connecting:
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
audioButtonAppearance = .color(.custom(0x1c1c1e))
case .connected:
actionButtonState = .active(state: isMicOn ? .on : .muted)
if isMicOn {
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
actionButtonSubtitle = ""
audioButtonAppearance = .color(.custom(0x005720))
} else {
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
actionButtonSubtitle = self.presentationData.strings.VoiceChat_UnmuteHelp
audioButtonAppearance = .color(.custom(0x00274d))
}
}
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
audioButtonAppearance = .color(.custom(0x1c1c1e))
}
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 144.0, height: 144.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, animated: true)
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
var audioMode: CallControllerButtonsSpeakerMode = .none
//var hasAudioRouteMenu: Bool = false
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
@ -556,13 +773,13 @@ public final class VoiceChatController: ViewController {
}
let soundImage: CallControllerButtonItemNode.Content.Image
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = .color(.grayDimmed)
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = audioButtonAppearance
switch audioMode {
case .none, .builtin:
soundImage = .speaker
case .speaker:
soundImage = .speaker
soundAppearance = .blurred(isFilled: true)
// soundAppearance = .blurred(isFilled: false)
case .headphones:
soundImage = .bluetooth
case let .bluetooth(type):
@ -576,46 +793,13 @@ public final class VoiceChatController: ViewController {
}
}
//TODO:localize
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: "audio", transition: .immediate)
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: self.presentationData.strings.VoiceChat_Audio, transition: .animated(duration: 0.4, curve: .linear))
//TODO:localize
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.redDimmed), image: .end), text: "leave", transition: .immediate)
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0x4d120e)), image: .end), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonInset, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonInset - sideButtonSize.width, y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
var isMicOn = false
if let callState = callState {
isMicOn = !callState.isMuted
switch callState.networkState {
case .connecting:
self.radialStatus.isHidden = false
self.statusLabel.attributedText = NSAttributedString(string: "Connecting...", font: Font.regular(17.0), textColor: .white)
case .connected:
self.radialStatus.isHidden = true
if isMicOn {
self.statusLabel.attributedText = NSAttributedString(string: "You're Live", font: Font.regular(17.0), textColor: .white)
} else {
self.statusLabel.attributedText = NSAttributedString(string: "Unmute", font: Font.regular(17.0), textColor: .white)
}
}
let statusSize = self.statusLabel.updateLayout(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude))
self.statusLabel.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: actionButtonFrame.maxY + 12.0), size: statusSize)
self.radialStatus.transitionToState(.progress(color: UIColor(rgb: 0x00ACFF), lineWidth: 3.3, value: nil, cancelEnabled: false), animated: false)
self.radialStatus.frame = actionButtonFrame.insetBy(dx: -3.3, dy: -3.3)
}
self.actionButton.updateLayout(size: centralButtonSize, isOn: isMicOn)
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
if isFirstTime {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
@ -628,11 +812,17 @@ public final class VoiceChatController: ViewController {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
@ -674,6 +864,19 @@ public final class VoiceChatController: ViewController {
strongSelf.didSetContentsReady = true
strongSelf.controller?.contentsReady.set(true)
}
if !transition.deletions.isEmpty || !transition.insertions.isEmpty {
var itemHeight: CGFloat = 56.0
strongSelf.listNode.forEachVisibleItemNode { node in
if node.frame.height > 0 {
itemHeight = node.frame.height
}
}
strongSelf.maxListHeight = CGFloat(transition.count) * itemHeight
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
})
}
@ -706,6 +909,10 @@ public final class VoiceChatController: ViewController {
var index: Int32 = 0
for member in members {
if let user = member.peer as? TelegramUser, user.botInfo != nil || user.isDeleted {
continue
}
let memberState: PeerEntry.State
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
if member.peer.id == self.context.account.peerId {
@ -722,7 +929,7 @@ public final class VoiceChatController: ViewController {
}
entries.append(PeerEntry(
participant: member,
peer: member.peer,
activityTimestamp: Int32.max - 1 - index,
state: memberState,
muteState: memberMuteState
@ -732,11 +939,59 @@ public final class VoiceChatController: ViewController {
self.currentEntries = entries
let presentationData = ItemListPresentationData(theme: self.darkTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings)
let presentationData = self.presentationData.withUpdated(theme: self.darkTheme)
let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, crossFade: false, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!)
self.enqueueTransition(transition)
}
@objc private func panGesture(_ recognizer: CallPanGestureRecognizer) {
switch recognizer.state {
case .began:
guard let (layout, _) = self.validLayout else {
return
}
self.contentContainer.clipsToBounds = true
self.contentContainer.cornerRadius = layout.deviceMetrics.screenCornerRadius
case .changed:
let offset = recognizer.translation(in: self.view).y
var bounds = self.bounds
bounds.origin.y = -offset
let transition = offset / bounds.height
if transition > 0.02 {
self.controller?.statusBar.statusBarStyle = .Ignore
} else {
self.controller?.statusBar.statusBarStyle = .White
}
self.bounds = bounds
case .cancelled, .ended:
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 200.0 {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.contentContainer.cornerRadius = 0.0
})
self.controller?.statusBar.statusBarStyle = .White
} else {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
self?.controller?.dismissInteractively()
var initialBounds = bounds
initialBounds.origin = CGPoint()
self?.bounds = initialBounds
self?.controller?.statusBar.statusBarStyle = .White
})
}
default:
break
}
}
}
private let sharedContext: SharedAccountContext
@ -762,13 +1017,13 @@ public final class VoiceChatController: ViewController {
self.call = call
self.presentationData = sharedContext.currentPresentationData.with { $0 }
let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: "Chat", target: self, action: #selector(self.closePressed))
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.VoiceChat_BackTitle, target: self, action: #selector(self.closePressed))
self.navigationItem.leftBarButtonItem = backItem
self.statusBar.statusBarStyle = .White
@ -814,6 +1069,16 @@ public final class VoiceChatController: ViewController {
}
}
func dismissInteractively(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.didAppearOnce = false
completion?()
self.presentingViewController?.dismiss(animated: false)
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
@ -832,3 +1097,25 @@ public final class VoiceChatController: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
}
}
private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = keepInPlace
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -0,0 +1,200 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private final class VoiceChatMicrophoneNodeDrawingState: NSObject {
let color: UIColor
let transition: CGFloat
let reverse: Bool
init(color: UIColor, transition: CGFloat, reverse: Bool) {
self.color = color
self.transition = transition
self.reverse = reverse
super.init()
}
}
final class VoiceChatMicrophoneNode: ASDisplayNode {
class State: Equatable {
let muted: Bool
let color: UIColor
init(muted: Bool, color: UIColor) {
self.muted = muted
self.color = color
}
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.muted != rhs.muted {
return false
}
if lhs.color.argb != rhs.color.argb {
return false
}
return true
}
}
private class TransitionContext {
let startTime: Double
let duration: Double
let previousState: State
init(startTime: Double, duration: Double, previousState: State) {
self.startTime = startTime
self.duration = duration
self.previousState = previousState
}
}
private var animator: ConstantDisplayLinkAnimator?
private var hasState = false
private var state: State = State(muted: false, color: .black)
private var transitionContext: TransitionContext?
override init() {
super.init()
self.isOpaque = false
}
func update(state: State, animated: Bool) {
var animated = animated
if !self.hasState {
self.hasState = true
animated = false
}
if self.state != state {
let previousState = self.state
self.state = state
if animated {
self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState)
}
self.updateAnimations()
self.setNeedsDisplay()
}
}
private func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
if let transitionContext = self.transitionContext {
if transitionContext.startTime + transitionContext.duration < timestamp {
self.transitionContext = nil
} else {
animate = true
}
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations()
})
self.animator = animator
}
animator.isPaused = false
} else {
self.animator?.isPaused = true
}
self.setNeedsDisplay()
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
var transitionFraction: CGFloat = self.state.muted ? 1.0 : 0.0
var color = self.state.color
var reverse = false
if let transitionContext = self.transitionContext {
let timestamp = CACurrentMediaTime()
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
t = min(1.0, max(0.0, t))
if transitionContext.previousState.muted != self.state.muted {
transitionFraction = self.state.muted ? t : 1.0 - t
reverse = transitionContext.previousState.muted
}
if transitionContext.previousState.color.rgb != color.rgb {
color = transitionContext.previousState.color.interpolateTo(color, fraction: t)!
}
}
return VoiceChatMicrophoneNodeDrawingState(color: color, transition: transitionFraction, reverse: reverse)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? VoiceChatMicrophoneNodeDrawingState else {
return
}
context.setFillColor(parameters.color.cgColor)
if bounds.size.width > 36.0 {
context.scaleBy(x: 2.5, y: 2.5)
}
context.translateBy(x: 18.0, y: 18.0)
let _ = try? drawSvgPath(context, path: "M-0.004000000189989805,-9.86400032043457 C2.2960000038146973,-9.86400032043457 4.165999889373779,-8.053999900817871 4.25600004196167,-5.77400016784668 C4.25600004196167,-5.77400016784668 4.265999794006348,-5.604000091552734 4.265999794006348,-5.604000091552734 C4.265999794006348,-5.604000091552734 4.265999794006348,-0.8040000200271606 4.265999794006348,-0.8040000200271606 C4.265999794006348,1.555999994277954 2.3559999465942383,3.4660000801086426 -0.004000000189989805,3.4660000801086426 C-2.2939999103546143,3.4660000801086426 -4.164000034332275,1.6460000276565552 -4.263999938964844,-0.6240000128746033 C-4.263999938964844,-0.6240000128746033 -4.263999938964844,-0.8040000200271606 -4.263999938964844,-0.8040000200271606 C-4.263999938964844,-0.8040000200271606 -4.263999938964844,-5.604000091552734 -4.263999938964844,-5.604000091552734 C-4.263999938964844,-7.953999996185303 -2.3540000915527344,-9.86400032043457 -0.004000000189989805,-9.86400032043457 Z ")
context.setBlendMode(.clear)
let _ = try? drawSvgPath(context, path: "M0.004000000189989805,-8.53600025177002 C-1.565999984741211,-8.53600025177002 -2.8459999561309814,-7.306000232696533 -2.936000108718872,-5.75600004196167 C-2.936000108718872,-5.75600004196167 -2.936000108718872,-5.5960001945495605 -2.936000108718872,-5.5960001945495605 C-2.936000108718872,-5.5960001945495605 -2.936000108718872,-0.7960000038146973 -2.936000108718872,-0.7960000038146973 C-2.936000108718872,0.8240000009536743 -1.6260000467300415,2.134000062942505 0.004000000189989805,2.134000062942505 C1.5740000009536743,2.134000062942505 2.8540000915527344,0.9039999842643738 2.934000015258789,-0.6460000276565552 C2.934000015258789,-0.6460000276565552 2.934000015258789,-0.7960000038146973 2.934000015258789,-0.7960000038146973 C2.934000015258789,-0.7960000038146973 2.934000015258789,-5.5960001945495605 2.934000015258789,-5.5960001945495605 C2.934000015258789,-7.22599983215332 1.6239999532699585,-8.53600025177002 0.004000000189989805,-8.53600025177002 Z ")
context.setBlendMode(.normal)
let _ = try? drawSvgPath(context, path: "M6.796000003814697,-1.4639999866485596 C7.165999889373779,-1.4639999866485596 7.466000080108643,-1.1640000343322754 7.466000080108643,-0.8040000200271606 C7.466000080108643,3.0959999561309814 4.47599983215332,6.296000003814697 0.6660000085830688,6.636000156402588 C0.6660000085830688,6.636000156402588 0.6660000085830688,9.196000099182129 0.6660000085830688,9.196000099182129 C0.6660000085830688,9.565999984741211 0.3659999966621399,9.866000175476074 -0.004000000189989805,9.866000175476074 C-0.33399999141693115,9.866000175476074 -0.6140000224113464,9.605999946594238 -0.6539999842643738,9.28600025177002 C-0.6539999842643738,9.28600025177002 -0.6639999747276306,9.196000099182129 -0.6639999747276306,9.196000099182129 C-0.6639999747276306,9.196000099182129 -0.6639999747276306,6.636000156402588 -0.6639999747276306,6.636000156402588 C-4.473999977111816,6.296000003814697 -7.464000225067139,3.0959999561309814 -7.464000225067139,-0.8040000200271606 C-7.464000225067139,-1.1640000343322754 -7.164000034332275,-1.4639999866485596 -6.803999900817871,-1.4639999866485596 C-6.434000015258789,-1.4639999866485596 -6.133999824523926,-1.1640000343322754 -6.133999824523926,-0.8040000200271606 C-6.133999824523926,2.5859999656677246 -3.384000062942505,5.335999965667725 -0.004000000189989805,5.335999965667725 C3.385999917984009,5.335999965667725 6.136000156402588,2.5859999656677246 6.136000156402588,-0.8040000200271606 C6.136000156402588,-1.1640000343322754 6.435999870300293,-1.4639999866485596 6.796000003814697,-1.4639999866485596 Z ")
context.translateBy(x: -18.0, y: -18.0)
if parameters.transition > 0.0 {
let startPoint: CGPoint
let endPoint: CGPoint
if parameters.reverse {
startPoint = CGPoint(x: 9.0 + 17.0 * (1.0 - parameters.transition), y: 10.0 - UIScreenPixel + 17.0 * (1.0 - parameters.transition))
endPoint = CGPoint(x: 26.0, y: 27.0 - UIScreenPixel)
} else {
startPoint = CGPoint(x: 9.0, y: 10.0 - UIScreenPixel)
endPoint = CGPoint(x: 9.0 + 17.0 * parameters.transition, y: 10.0 - UIScreenPixel + 17.0 * parameters.transition)
}
context.setBlendMode(.clear)
context.setLineWidth(4.0)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
context.setBlendMode(.normal)
context.setStrokeColor(parameters.color.cgColor)
context.setLineWidth(1.0 + UIScreenPixel)
context.setLineCap(.round)
context.setLineJoin(.round)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
}
}
}

View File

@ -0,0 +1,63 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
final class VoiceChatOptionsButton: HighlightableButtonNode {
let extractedContainerNode: ContextExtractedContentContainingNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init() {
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
super.init()
self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.iconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addSubnode(self.containerNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.iconNode.image = generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(x: 6.0, y: 12.0, width: 4.0, height: 4.0))
context.fillEllipse(in: CGRect(x: 12.0, y: 12.0, width: 4.0, height: 4.0))
context.fillEllipse(in: CGRect(x: 18.0, y: 12.0, width: 4.0, height: 4.0))
})
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
self.extractedContainerNode.frame = self.containerNode.bounds
self.iconNode.frame = self.containerNode.bounds
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 28.0, height: 28.0)
}
func onLayout() {
}
}

View File

@ -0,0 +1,669 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AvatarNode
import TelegramStringFormatting
import PeerPresenceStatusManager
import ContextUI
import AccountContext
import LegacyComponents
import AudioBlob
public final class VoiceChatParticipantItem: ListViewItem {
public enum ParticipantText {
public enum TextColor {
case generic
case accent
case constructive
}
case presence
case text(String, TextColor)
case none
}
public enum Icon {
case none
case microphone(Bool, UIColor)
case invite
}
let presentationData: ItemListPresentationData
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let context: AccountContext
let peer: Peer
let presence: PeerPresence?
let text: ParticipantText
let icon: Icon
let enabled: Bool
let audioLevel: Signal<Float, NoError>?
let action: (() -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, presence: PeerPresence?, text: ParticipantText, icon: Icon, enabled: Bool, audioLevel: Signal<Float, NoError>?, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.context = context
self.peer = peer
self.presence = presence
self.text = text
self.icon = icon
self.enabled = enabled
self.audioLevel = audioLevel
self.action = action
self.contextAction = contextAction
}
public var selectable: Bool = false
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 = VoiceChatParticipantItemNode()
let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil, nextItem == nil)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (node.avatarNode.ready, { _ in apply(synchronousLoads, false) })
})
}
}
}
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? VoiceChatParticipantItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, previousItem == nil, nextItem == nil)
Queue.mainQueue().async {
completion(layout, { _ in
apply(false, animated)
})
}
}
}
}
}
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()
}
}
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
public class VoiceChatParticipantItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private let offsetContainerNode: ASDisplayNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
fileprivate let avatarNode: AvatarNode
private let titleNode: TextNode
private let statusNode: TextNode
private let actionContainerNode: ASDisplayNode
private var animationNode: VoiceChatMicrophoneNode?
private var actionButtonNode: HighlightableButtonNode?
private var audioLevelView: VoiceBlobView?
private let audioLevelDisposable = MetaDisposable()
private var absoluteLocation: (CGRect, CGSize)?
private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (VoiceChatParticipantItem, ListViewItemLayoutParams, Bool, Bool)?
override public var canBeSelected: Bool {
return false
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.offsetContainerNode = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.statusNode = TextNode()
self.statusNode.isUserInteractionEnabled = false
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
self.actionContainerNode = ASDisplayNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.avatarNode)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.statusNode)
self.offsetContainerNode.addSubnode(self.actionContainerNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3)
apply(false, true)
}
})
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self else {
return false
}
if let actionButtonNode = strongSelf.actionButtonNode, actionButtonNode.frame.contains(location) {
return false
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
deinit {
self.audioLevelDisposable.dispose()
}
public func asyncLayout() -> (_ item: VoiceChatParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
var currentDisabledOverlayNode = self.disabledOverlayNode
let currentItem = self.layoutParams?.0
return { item, params, first, last in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let statusFont = Font.regular(statusFontSize)
var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString?
let rightInset: CGFloat = params.rightInset
let titleColor = item.presentationData.theme.list.itemPrimaryTextColor
let currentBoldFont: UIFont = titleFont
if let user = item.peer as? TelegramUser {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
let string = NSMutableAttributedString()
switch item.nameDisplayOrder {
case .firstLast:
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
case .lastFirst:
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
}
titleAttributedString = string
} else if let firstName = user.firstName, !firstName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: currentBoldFont, textColor: titleColor)
} else if let lastName = user.lastName, !lastName.isEmpty {
titleAttributedString = NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor)
} else {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: currentBoldFont, textColor: titleColor)
}
} else if let group = item.peer as? TelegramGroup {
titleAttributedString = NSAttributedString(string: group.title, font: currentBoldFont, textColor: titleColor)
} else if let channel = item.peer as? TelegramChannel {
titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor)
}
switch item.text {
case .presence:
if let user = item.peer as? TelegramUser, let botInfo = user.botInfo {
let botStatus: String
if botInfo.flags.contains(.hasAccessToChatHistory) {
botStatus = item.presentationData.strings.Bot_GroupStatusReadsHistory
} else {
botStatus = item.presentationData.strings.Bot_GroupStatusDoesNotReadHistory
}
statusAttributedString = NSAttributedString(string: botStatus, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else if let presence = item.presence as? TelegramUserPresence {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let (string, _) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.LastSeen_Offline, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .text(text, textColor):
let textColorValue: UIColor
switch textColor {
case .generic:
textColorValue = item.presentationData.theme.list.itemSecondaryTextColor
case .accent:
textColorValue = item.presentationData.theme.list.itemAccentColor
case .constructive:
textColorValue = UIColor(rgb: 0x34c759)
}
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
case .none:
break
}
let leftInset: CGFloat = 65.0 + params.leftInset
let verticalInset: CGFloat = 8.0
let verticalOffset: CGFloat = 0.0
let avatarSize: CGFloat = 40.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = UIEdgeInsets()
let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height
let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5)
}
} else {
currentDisabledOverlayNode = nil
}
var animateStatusTransitionFromUp: Bool?
if let currentItem = currentItem {
if case .presence = currentItem.text, case let .text(_, newColor) = item.text {
animateStatusTransitionFromUp = newColor == .constructive
} else if case let .text(_, currentColor) = currentItem.text, case let .text(_, newColor) = item.text, currentColor != newColor {
animateStatusTransitionFromUp = newColor == .constructive
} else if case .text = currentItem.text, case .presence = item.text {
animateStatusTransitionFromUp = false
}
}
return (layout, { [weak self] synchronousLoad, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, first, last)
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
strongSelf.actionContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.accessibilityLabel = titleAttributedString?.string
var combinedValueString = ""
if let statusString = statusAttributedString?.string, !statusString.isEmpty {
combinedValueString.append(statusString)
}
strongSelf.accessibilityValue = combinedValueString
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.addSubnode(currentDisabledOverlayNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let animateStatusTransitionFromUp = animateStatusTransitionFromUp {
let offset: CGFloat = animateStatusTransitionFromUp ? -7.0 : 7.0
if let snapshotView = strongSelf.statusNode.view.snapshotContentTree() {
strongSelf.statusNode.view.superview?.insertSubview(snapshotView, belowSubview: strongSelf.statusNode.view)
snapshotView.frame = strongSelf.statusNode.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.2, removeOnCompletion: false, additive: true)
strongSelf.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.statusNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.2, additive: true)
}
}
let _ = titleApply()
let _ = statusApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
strongSelf.topStripeNode.isHidden = first
strongSelf.bottomStripeNode.isHidden = last
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height + -separatorHeight), size: CGSize(width: layoutSize.width - leftInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + verticalOffset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
let blobFrame = avatarFrame.insetBy(dx: -12.0, dy: -12.0)
if let audioLevel = item.audioLevel {
strongSelf.audioLevelView?.frame = blobFrame
strongSelf.audioLevelDisposable.set((audioLevel
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
if strongSelf.audioLevelView == nil {
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 0.3,
smallBlobRange: (0, 0),
mediumBlobRange: (0.7, 0.8),
bigBlobRange: (0.8, 0.9)
)
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
let playbackMaskLayer = CAShapeLayer()
playbackMaskLayer.frame = maskRect
playbackMaskLayer.fillRule = .evenOdd
let maskPath = UIBezierPath()
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
maskPath.append(UIBezierPath(rect: maskRect))
playbackMaskLayer.path = maskPath.cgPath
audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(.green)
strongSelf.audioLevelView = audioLevelView
strongSelf.containerNode.view.insertSubview(audioLevelView, at: 0)
}
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
if value > 0.0 {
strongSelf.audioLevelView?.startAnimating()
} else {
strongSelf.audioLevelView?.stopAnimating()
}
}))
} else if let audioLevelView = strongSelf.audioLevelView {
strongSelf.audioLevelView = nil
audioLevelView.removeFromSuperview()
strongSelf.audioLevelDisposable.set(nil)
}
var overrideImage: AvatarNodeImageOverride?
if item.peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
if case let .microphone(muted, color) = item.icon {
let animationNode: VoiceChatMicrophoneNode
if let current = strongSelf.animationNode {
animationNode = current
} else {
animationNode = VoiceChatMicrophoneNode()
strongSelf.animationNode = animationNode
strongSelf.actionContainerNode.addSubnode(animationNode)
}
animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, color: color), animated: true)
} else if let animationNode = strongSelf.animationNode {
strongSelf.animationNode = nil
animationNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in
animationNode?.removeFromSupernode()
})
}
if case .invite = item.icon {
let actionButtonNode: HighlightableButtonNode
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
actionButtonNode = HighlightableButtonNode()
actionButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: item.presentationData.theme.list.itemAccentColor), for: .normal)
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
strongSelf.actionButtonNode = actionButtonNode
strongSelf.actionContainerNode.addSubnode(actionButtonNode)
}
} else if let actionButtonNode = strongSelf.actionButtonNode {
strongSelf.actionButtonNode = nil
actionButtonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak actionButtonNode] _ in
actionButtonNode?.removeFromSupernode()
})
}
let animationSize = CGSize(width: 36.0, height: 36.0)
strongSelf.animationNode?.frame = CGRect(x: params.width - animationSize.width - 6.0, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height)
strongSelf.actionButtonNode?.frame = CGRect(x: params.width - animationSize.width - 6.0, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height)
if let presence = item.presence as? TelegramUserPresence {
strongSelf.peerPresenceManager?.reset(presence: presence)
}
strongSelf.updateIsHighlighted(transition: transition)
}
})
}
}
var isHighlighted = false
var reallyHighlighted: Bool {
var reallyHighlighted = self.isHighlighted
return reallyHighlighted
}
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
if self.reallyHighlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if transition.isAnimated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func header() -> ListViewItemHeader? {
return nil
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
}
@objc private func actionButtonPressed() {
if let item = self.layoutParams?.0 {
item.action?()
}
}
}

View File

@ -672,3 +672,32 @@ extension GroupCallParticipantsContext.StateUpdate {
)
}
}
public enum InviteToGroupCallError {
case generic
}
public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64, peerId: PeerId) -> Signal<Never, InviteToGroupCallError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> castError(InviteToGroupCallError.self)
|> mapToSignal { user -> Signal<Never, InviteToGroupCallError> in
guard let user = user else {
return .fail(.generic)
}
guard let apiUser = apiInputUser(user) else {
return .fail(.generic)
}
return account.network.request(Api.functions.phone.inviteToGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), userId: apiUser))
|> mapError { _ -> InviteToGroupCallError in
return .generic
}
|> mapToSignal { result -> Signal<Never, InviteToGroupCallError> in
account.stateManager.addUpdates(result)
return .complete()
}
}
}

View File

@ -64,8 +64,11 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
case let .inputGroupCall(id, accessHash):
return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, duration: duration))
}
case .messageActionInviteToGroupCall:
return nil
case let .messageActionInviteToGroupCall(call, userId):
switch call {
case let .inputGroupCall(id, accessHash):
return TelegramMediaAction(action: .inviteToGroupPhoneCall(callId: id, accessHash: accessHash, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)))
}
}
}

View File

@ -5,6 +5,7 @@ import SyncCore
import TelegramUIPreferences
public let defaultDarkPresentationTheme = makeDefaultDarkPresentationTheme(preview: false)
public let defaultDarkColorPresentationTheme = customizeDefaultDarkPresentationTheme(theme: defaultDarkPresentationTheme, editing: false, title: nil, accentColor: UIColor(rgb: 0x007aff), backgroundColors: nil, bubbleColors: nil, wallpaper: nil)
public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme {
if (theme.referenceTheme != .night) {

View File

@ -90,6 +90,10 @@ public final class PresentationData: Equatable {
self.largeEmoji = largeEmoji
}
public func withUpdated(theme: PresentationTheme) -> PresentationData {
return PresentationData(strings: self.strings, theme: theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, disableAnimations: self.disableAnimations, largeEmoji: self.largeEmoji)
}
public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool {
return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.disableAnimations == rhs.disableAnimations && lhs.largeEmoji == rhs.largeEmoji
}

View File

@ -460,6 +460,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
attributedString = addAttributesToStringWithRanges(strings.Notification_ProximityReached(message.peers[fromId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "", distanceString, message.peers[toId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, fromId), (2, toId)]))
}
case let .inviteToGroupPhoneCall(_, _, userId):
if message.author?.id == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_VoiceChatInvitationByYou( message.peers[userId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, userId)]))
} else if userId == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_VoiceChatInvitationForYou(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]))
} else {
attributedString = addAttributesToStringWithRanges(strings.Notification_VoiceChatInvitation(authorName, message.peers[userId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id), (1, userId)]))
}
case .unknown:
attributedString = nil
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_invited.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Voice.pdf",
"filename" : "ic_mute.pdf",
"idiom" : "universal"
}
],

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Voice.pdf",
"filename" : "ic_unmute.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_menucheck.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1 @@
{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":72,"h":72,"nm":"ic_mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36,37.2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}

View File

@ -20,9 +20,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
}
}
}
var tapped: (() -> Void)?
override init() {
self.containerNode = ContextControllerSourceNode()
self.avatarNode = AvatarNode(font: normalFont)
@ -41,9 +39,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0)
self.avatarNode.frame = self.containerNode.bounds
/*self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0))
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0))*/
}
override func didLoad() {
@ -51,19 +46,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
self.view.isOpaque = false
}
@objc private func avatarTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
self.tapped?()
default:
break
}
}
}
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 37.0, height: 37.0)
}

View File

@ -157,6 +157,13 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
case .trendingGifs:
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
case let .gifEmoji(emoji):
var emoji = emoji
if emoji == "🥳" {
if #available(iOSApplicationExtension 12.1, iOS 12.1, *) {
} else {
emoji = "🎉"
}
}
self.imageNode.image = nil
self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(27.0), textColor: .black)
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))

View File

@ -772,7 +772,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount)
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount)
var isReplyThread = false
if case .replyThread = item.chatLocation {

View File

@ -431,7 +431,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
if item.message.id.peerId != item.context.account.peerId && !item.message.id.peerId.isReplies{
if item.message.id.peerId != item.context.account.peerId && !item.message.id.peerId.isReplies {
for attribute in item.message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let sourcePeer = item.message.peers[attribute.messageId.peerId] {
@ -457,7 +457,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
var updatedShareButtonNode: ChatMessageShareButton?
if needShareButton {
if currentShareButtonNode != nil {
if let currentShareButtonNode = currentShareButtonNode {
updatedShareButtonNode = currentShareButtonNode
} else {
let buttonNode = ChatMessageShareButton()

View File

@ -88,9 +88,6 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - 56.0, height: .greatestFiniteMagnitude))
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 12.0), size: textSize)
let height = textSize.height + 24.0
@ -103,7 +100,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
}
let switchSize = switchView.bounds.size
self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - safeInsets.right, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: !firstTime)
}

View File

@ -2010,7 +2010,6 @@ final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
var buttonTransition = transition
if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 {
buttonTransition = .animated(duration: duration * 0.25, curve: curve)
@ -3131,7 +3130,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale)
transition.updateSublayerTransformScale(node: self.usernameNodeContainer, scale: subtitleScale)
} else {
let titleScale: CGFloat
let subtitleScale: CGFloat

View File

@ -5266,7 +5266,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
bottomInset = max(bottomInset, selectionPanelNode.bounds.height)
}
let navigationBarHeight: CGFloat = layout.isModalOverlay ? 56.0 : 44.0
let navigationBarHeight: CGFloat = !self.isSettings && layout.isModalOverlay ? 56.0 : 44.0
self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: effectiveAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition)
self.headerNode.navigationButtonContainer.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: layout.statusBarHeight ?? 0.0), size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: navigationBarHeight))
self.headerNode.navigationButtonContainer.isWhite = self.headerNode.isAvatarExpanded

View File

@ -1,471 +0,0 @@
import Foundation
import UIKit
import Display
import LegacyComponents
private struct Constants {
static let sineWaveSpeed: CGFloat = 0.81
static let smallWaveRadius: CGFloat = 0.55
static let smallWaveScale: CGFloat = 0.40
static let smallWaveScaleSpeed: CGFloat = 0.6
static let flingDistance: CGFloat = 0.5
static let circleRadius: CGFloat = 56.0
static let animationSpeed: CGFloat = 0.35 * 0.1
static let animationSpeedSmall: CGFloat = 0.55 * 0.1
static let rotationSpeed: CGFloat = 0.36 * 0.1
static let waveAngle: CGFloat = 0.03
static let randomRadiusSize: CGFloat = 0.3
static let idleWaveAngle: CGFloat = 0.5
static let idleScaleSpeed: CGFloat = 0.3
static let idleRotationSpeed: CGFloat = 0.2
static let idleRadiusValue: CGFloat = 0.56
static let idleRotationDiff: CGFloat = 0.1 * idleRotationSpeed
}
class CombinedWaveView: UIView {
private let bigWaveView: WaveView
private let smallWaveView: WaveView
private var level: CGFloat = 0.0
init(frame: CGRect, color: UIColor) {
let n = 12
let bounds = CGRect(origin: CGPoint(), size: frame.size)
self.bigWaveView = WaveView(frame: bounds, n: n, amplitudeRadius: 30.0, isBig: true, color: color.withAlphaComponent(0.3))
self.smallWaveView = WaveView(frame: bounds, n: n, amplitudeRadius: 35.0, isBig: false, color: color.withAlphaComponent(0.15))
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.bigWaveView.rotation = CGFloat.pi / 6.0
self.bigWaveView.amplitudeWaveDif = 0.02 * Constants.sineWaveSpeed * CGFloat.pi / 180.0
self.smallWaveView.amplitudeWaveDif = 0.026 * Constants.sineWaveSpeed
self.smallWaveView.amplitudeRadius = 10.0 + 20.0 * Constants.smallWaveRadius
self.smallWaveView.maxScale = 0.3 * Constants.smallWaveScale
self.smallWaveView.scaleSpeed = 0.001 * Constants.smallWaveScaleSpeed
self.smallWaveView.fling = Constants.flingDistance
self.addSubview(self.bigWaveView)
self.addSubview(self.smallWaveView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateLevel(_ level: CGFloat) {
let level = level * 0.2
self.level = level
self.bigWaveView.setLevel(level)
self.smallWaveView.setLevel(level)
}
func tick(_ level: CGFloat) {
let radius = 56.0 + 30.0 * level * 0.2
self.bigWaveView.tick(circleRadius: radius)
self.smallWaveView.tick(circleRadius: radius)
}
func setColor(_ color: UIColor) {
}
}
class WaveView : UIView {
var fling: CGFloat = 0.0
private var animateToAmplitude: CGFloat = 0.0
private var amplitude: CGFloat = 0.0
private var slowAmplitude: CGFloat = 0.0
private var animateAmplitudeDiff: CGFloat = 0.0
private var animateAmplitudeSlowDiff: CGFloat = 0.0
private var lastRadius: CGFloat = 0.0
private var radiusDiff: CGFloat = 0.0
private var waveDiff: CGFloat = 0.0
private var waveAngle: CGFloat = 0.0
private var incRandomAdditionals = false
var rotation: CGFloat = 0.0
private var idleRotation: CGFloat = 0.0
private var innerRotation: CGFloat = 0.0
var amplitudeWaveDif: CGFloat = 0.0
var amplitudeRadius: CGFloat
private let isBig: Bool
private var idleRadius: CGFloat = 0.0
private var idleRadiusK: CGFloat = 0.15 * Constants.idleWaveAngle
private var expandIdleRadius = false
private var expandScale = false
private var isIdle = true
private var scale: CGFloat = 1.0
private var scaleIdleDif: CGFloat = 0.0
private var scaleDif: CGFloat = 0.0
var scaleSpeed: CGFloat = 0.00008
public var scaleSpeedIdle: CGFloat = 0.0002 * Constants.idleScaleSpeed
var maxScale: CGFloat = 0.0
private var flingRadius: CGFloat = 0.0
private let randomAdditions: CGFloat = 8.0 * Constants.randomRadiusSize
private var idleGlobalRadius: CGFloat = 10.0 * Constants.idleRadiusValue
private var sineAngleMax: CGFloat = 0.0
private let n: Int
private let l: CGFloat
private var additions: [CGFloat]
var idleStateDiff: CGFloat = 0.0
var radius: CGFloat = 60.0;
var cubicBezierK: CGFloat = 1.0;
var randomK: CGFloat = 0.0
var color: UIColor
init(frame: CGRect, n: Int, amplitudeRadius: CGFloat, isBig: Bool, color: UIColor) {
self.n = n
self.amplitudeRadius = amplitudeRadius
self.isBig = isBig
self.color = color
self.expandIdleRadius = isBig
self.radiusDiff = 34.0 * 0.0012
self.l = 4.0 / 3.0 * tan(CGFloat.pi / (2.0 * CGFloat(self.n)))
self.additions = Array(repeating: 0.0, count: self.n)
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
self.updateAdditions()
}
func setLevel(_ level: CGFloat) {
self.animateToAmplitude = level
let amplitudeDelta: CGFloat
let amplitudeSlowDelta: CGFloat
if self.isBig {
if self.animateToAmplitude > self.amplitude {
amplitudeDelta = 300.0 * Constants.animationSpeed
amplitudeSlowDelta = 500.0 * Constants.animationSpeed
} else {
amplitudeDelta = 500.0 * Constants.animationSpeed
amplitudeSlowDelta = 500.0 * Constants.animationSpeed
}
} else {
if self.animateToAmplitude > self.amplitude {
amplitudeDelta = 400.0 * Constants.animationSpeedSmall
amplitudeSlowDelta = 500.0 * Constants.animationSpeedSmall
} else {
amplitudeDelta = 500.0 * Constants.animationSpeedSmall
amplitudeSlowDelta = 500.0 * Constants.animationSpeedSmall
}
}
self.animateAmplitudeDiff = (self.animateToAmplitude - self.amplitude) / (100.0 + amplitudeDelta)
self.animateAmplitudeSlowDiff = (self.animateToAmplitude - self.slowAmplitude) / (100.0 + amplitudeSlowDelta)
let isIdle = level < 0.1
if self.isIdle != isIdle && isIdle && self.isBig {
//
//
//
}
self.isIdle = isIdle
}
private var wasFling = false
private func startFling(delta: CGFloat) {
self.pop_removeAnimation(forKey: "fling1")
self.pop_removeAnimation(forKey: "fling2")
let fling = self.fling * 2.0
let flingDistance = delta * self.amplitudeRadius * (self.isBig ? 8.0 : 20.0) * 16.0 * fling
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "fling1", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! WaveView).flingRadius
}
property?.writeBlock = { node, values in
(node as! WaveView).flingRadius = values!.pointee
}
property?.threshold = 0.01
}) as? POPAnimatableProperty
animation.fromValue = self.flingRadius as NSNumber
animation.toValue = flingDistance as NSNumber
animation.duration = Double((self.isBig ? 0.2 : 0.35) * fling)
animation.completionBlock = { [weak self] _, finished in
guard let strongSelf = self else {
return
}
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "fling2", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! WaveView).flingRadius
}
property?.writeBlock = { node, values in
(node as! WaveView).flingRadius = values!.pointee
}
property?.threshold = 0.01
}) as? POPAnimatableProperty
animation.fromValue = flingDistance as NSNumber
animation.toValue = 0.0 as NSNumber
animation.duration = Double((strongSelf.isBig ? 0.22 : 0.38) * fling)
strongSelf.pop_add(animation, forKey: "fling2")
}
self.pop_add(animation, forKey: "fling1")
}
private var lastUpdateTime: CGFloat?
func tick(circleRadius: CGFloat) {
let dt: CGFloat
let time = CGFloat(CACurrentMediaTime())
if let lastUpdateTime = self.lastUpdateTime {
dt = (time - lastUpdateTime) * 1000.0
} else {
dt = 0.0
}
self.lastUpdateTime = time
if self.animateToAmplitude != self.amplitude {
self.amplitude += self.animateAmplitudeDiff * dt
if self.animateAmplitudeDiff > 0.0 {
if self.amplitude > self.animateToAmplitude {
self.amplitude = self.animateToAmplitude
}
} else {
if self.amplitude < self.animateToAmplitude {
self.amplitude = self.animateToAmplitude
}
}
if abs(self.amplitude - self.animateToAmplitude) * self.amplitudeRadius < 4.0 {
if !self.wasFling {
self.startFling(delta: self.animateAmplitudeDiff)
self.wasFling = true
}
} else {
self.wasFling = false
}
}
if self.animateToAmplitude != self.slowAmplitude {
self.slowAmplitude += self.animateAmplitudeSlowDiff * dt
if abs(self.slowAmplitude - self.amplitude) > 0.2 {
self.slowAmplitude = self.amplitudeRadius + (self.slowAmplitude > self.amplitude ? 0.2 : -0.2)
}
if self.animateAmplitudeSlowDiff > 0.0 {
if self.slowAmplitude > self.animateToAmplitude {
self.slowAmplitude = self.animateToAmplitude
}
} else {
if self.slowAmplitude < self.animateToAmplitude {
self.slowAmplitude = self.animateToAmplitude
}
}
}
self.idleRadius = circleRadius * self.idleRadiusK
if self.expandIdleRadius {
self.scaleIdleDif += self.scaleSpeedIdle * dt
if self.scaleIdleDif >= 0.05 {
self.scaleIdleDif = 0.05
self.expandIdleRadius = false
}
} else {
self.scaleIdleDif -= self.scaleSpeedIdle * dt
if self.scaleIdleDif < 0.0 {
self.scaleIdleDif = 0.0
self.expandIdleRadius = true
}
}
if self.maxScale > 0.0 {
if self.expandScale {
self.scaleDif += self.scaleSpeed * dt
if self.scaleDif >= self.maxScale {
self.scaleDif = self.maxScale
self.expandScale = false
}
} else {
self.scaleDif -= self.scaleSpeed * dt
if self.scaleDif < 0.0 {
self.scaleDif = 0.0
self.expandScale = true
}
}
}
if self.sineAngleMax > self.animateToAmplitude {
self.sineAngleMax -= 0.25
if self.sineAngleMax < self.animateToAmplitude {
self.sineAngleMax = self.animateToAmplitude
}
} else if self.sineAngleMax < self.animateToAmplitude {
self.sineAngleMax += 0.25
if self.sineAngleMax > self.animateToAmplitude {
self.sineAngleMax = self.animateToAmplitude
}
}
if !self.isIdle {
self.rotation += (Constants.rotationSpeed * 0.5 + Constants.rotationSpeed * 4.0 * (self.amplitude > 0.5 ? 1.0 : self.amplitude / 0.5) * dt) * CGFloat.pi / 180.0
while self.rotation > CGFloat.pi * 2.0 {
self.rotation -= CGFloat.pi * 2.0
}
} else {
self.idleRotation += Constants.idleRotationDiff * dt * CGFloat.pi / 180.0
while self.idleRotation > CGFloat.pi * 2.0 {
self.idleRotation -= CGFloat.pi * 2.0
}
}
if self.lastRadius < circleRadius {
self.lastRadius = circleRadius
} else {
self.lastRadius -= self.radiusDiff * dt
if self.lastRadius < circleRadius {
self.lastRadius = circleRadius
}
}
self.lastRadius = circleRadius
if !self.isIdle {
self.waveAngle += self.amplitudeWaveDif * self.sineAngleMax * dt
if self.isBig {
self.waveDiff = cos(self.waveAngle)
} else {
self.waveDiff = -cos(self.waveAngle)
}
if self.waveDiff > 0.0 && self.incRandomAdditionals {
self.updateAdditions()
self.incRandomAdditionals = false
} else if self.waveDiff < 0.0 && !self.incRandomAdditionals {
self.updateAdditions()
self.incRandomAdditionals = true
}
}
self.prepareDraw()
}
func updateAdditions() {
self.additions = (0..<self.n).map { _ in CGFloat(arc4random() % 100) / 100.0 }
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func prepareDraw() {
let waveAmplitude = self.amplitude < 0.3 ? self.amplitude / 0.3 : 1.0
let radiusDiff: CGFloat = 10.0 + 50.0 * Constants.waveAngle * self.animateToAmplitude
self.idleStateDiff = self.idleRadius * (1.0 - waveAmplitude)
let kDiff: CGFloat = 0.35 * waveAmplitude * self.waveDiff
self.radiusDiff = radiusDiff * kDiff
self.cubicBezierK = 1.0 + abs(kDiff) * waveAmplitude + (1.0 - waveAmplitude) * self.idleRadiusK
self.radius = (self.lastRadius + self.amplitudeRadius * self.amplitude) + self.idleGlobalRadius + (self.flingRadius * waveAmplitude)
if self.radius + self.radiusDiff < Constants.circleRadius {
self.radiusDiff = Constants.circleRadius - self.radius
}
if self.isBig {
self.innerRotation = self.rotation + self.idleRotation
} else {
self.innerRotation = -self.rotation + self.idleRotation
}
self.randomK = waveAmplitude * self.waveDiff * self.randomAdditions
self.scale = 1.0 + self.scaleIdleDif * (1.0 - waveAmplitude) + self.scaleDif * waveAmplitude
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()!
ctx.clear(rect)
let r1 = self.radius - self.idleStateDiff / 2.0 - self.radiusDiff / 2.0
let r2 = self.radius + self.radiusDiff / 2.0 + self.idleStateDiff / 2.0
let l = self.l * max(r1, r2) * self.cubicBezierK
let cx = rect.width / 2.0
let cy = rect.height / 2.0
let path = UIBezierPath()
for i in 0..<self.n {
var transform = CGAffineTransform.init(translationX: cx, y: cy)
transform = transform.rotated(by: 2 * CGFloat.pi / CGFloat(self.n) * CGFloat(i))
transform = transform.translatedBy(x: -cx, y: -cy)
var r = ((i % 2 == 0) ? r1 : r2) + self.randomK * self.additions[i]
var p1 = CGPoint(x: cx, y: cy - r)
var p2 = CGPoint(x: cx + l + self.randomK * self.additions[i] * self.l, y: cy - r)
p1 = p1.applying(transform)
p2 = p2.applying(transform)
var j = i + 1
if j >= self.n {
j = 0
}
r = ((j % 2 == 0) ? r1 : r2) + self.randomK * self.additions[j]
var p3 = CGPoint(x: cx, y: cy - r)
var p4 = CGPoint(x: cx - l + self.randomK * self.additions[j] * self.l, y: cy - r)
transform = CGAffineTransform.init(translationX: cx, y: cy)
transform = transform.rotated(by: 2 * CGFloat.pi / CGFloat(self.n) * CGFloat(j))
transform = transform.translatedBy(x: -cx, y: -cy)
p3 = p3.applying(transform)
p4 = p4.applying(transform)
if i == 0 {
path.move(to: p1)
}
path.addCurve(to: p3, controlPoint1: p2, controlPoint2: p4)
}
ctx.setFillColor(self.color.cgColor)
ctx.saveGState()
ctx.translateBy(x: rect.width / 2.0, y: rect.height / 2.0)
ctx.scaleBy(x: self.scale, y: self.scale)
ctx.rotate(by: self.innerRotation)
ctx.translateBy(x: -rect.width / 2.0, y: -rect.height / 2.0)
ctx.addPath(path.cgPath)
ctx.drawPath(using: .fill)
ctx.restoreGState()
}
}

View File

@ -49,12 +49,14 @@ public final class OngoingGroupCallContext {
let isMuted = ValuePromise<Bool>(true, ignoreRepeated: true)
let memberStates = ValuePromise<[UInt32: MemberState]>([:], ignoreRepeated: true)
let audioLevels = ValuePipe<[(UInt32, Float)]>()
let myAudioLevel = ValuePipe<Float>()
init(queue: Queue) {
self.queue = queue
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)?
var myAudioLevelUpdatedImpl: ((Float) -> Void)?
self.context = GroupCallThreadLocalContext(
queue: ContextQueueImpl(queue: queue),
@ -63,6 +65,9 @@ public final class OngoingGroupCallContext {
},
audioLevelsUpdated: { levels in
audioLevelsUpdatedImpl?(levels)
},
myAudioLevelUpdated: { level in
myAudioLevelUpdatedImpl?(level)
}
)
@ -99,6 +104,13 @@ public final class OngoingGroupCallContext {
}
}
let myAudioLevel = self.myAudioLevel
myAudioLevelUpdatedImpl = { level in
queue.async {
myAudioLevel.putNext(level)
}
}
self.context.emitJoinPayload({ [weak self] payload, ssrc in
queue.async {
guard let strongSelf = self else {
@ -231,6 +243,18 @@ public final class OngoingGroupCallContext {
}
}
public var myAudioLevel: Signal<Float, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.myAudioLevel.signal().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public var isMuted: Signal<Bool, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()

View File

@ -158,7 +158,7 @@ typedef NS_ENUM(int32_t, GroupCallNetworkState) {
@interface GroupCallThreadLocalContext : NSObject
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated;
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated myAudioLevelUpdated:(void (^ _Nonnull)(float))myAudioLevelUpdated;
- (void)stop;

View File

@ -808,7 +808,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
@implementation GroupCallThreadLocalContext
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated {
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated myAudioLevelUpdated:(void (^ _Nonnull)(float))myAudioLevelUpdated {
self = [super init];
if (self != nil) {
_queue = queue;
@ -833,6 +833,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
[result addObject:@(it.second)];
}
audioLevelsUpdated(result);
},
.myAudioLevelUpdated = [myAudioLevelUpdated](float level) {
myAudioLevelUpdated(level);
}
}));
}

View File

@ -23,6 +23,8 @@ swift_library(
"//submodules/StickerResources:StickerResources",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
"//submodules/AvatarNode:AvatarNode",
"//submodules/AccountContext:AccountContext",
],
visibility = [
"//visibility:public",

View File

@ -5,6 +5,7 @@ import TelegramPresentationData
import SyncCore
import Postbox
import TelegramCore
import AccountContext
public enum UndoOverlayContent {
case removedChat(text: String)
@ -22,6 +23,7 @@ public enum UndoOverlayContent {
case chatRemovedFromFolder(chatTitle: String, folderTitle: String)
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)
}
public enum UndoOverlayAction {

View File

@ -3,6 +3,9 @@ import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import SyncCore
import TelegramCore
import TelegramPresentationData
import TextFormat
import Markdown
@ -12,15 +15,14 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SlotMachineAnimationNode
import AnimationUI
import SyncCore
import Postbox
import TelegramCore
import StickerResources
import AvatarNode
final class UndoOverlayControllerNode: ViewControllerTracingNode {
private let elevatedLayout: Bool
private var statusNode: RadialStatusNode?
private let timerTextNode: ImmediateTextNode
private let avatarNode: AvatarNode?
private let iconNode: ASImageNode?
private let iconCheckNode: RadialStatusNode?
private let animationNode: AnimationNode?
@ -85,6 +87,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content {
case let .removedChat(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
@ -94,6 +97,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.originalRemainingSeconds = 5
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
case let .archivedChat(_, title, text, undo):
self.avatarNode = nil
if undo {
self.iconNode = ASImageNode()
self.iconNode?.displayWithoutProcessing = true
@ -113,6 +117,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = undo
self.originalRemainingSeconds = 5
case let .hidArchive(title, text, undo):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_archiveswipe", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -122,6 +127,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = undo
self.originalRemainingSeconds = 3
case let .revealedArchive(title, text, undo):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -131,6 +137,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = undo
self.originalRemainingSeconds = 3
case let .succeed(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -144,6 +151,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 3
case let .info(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -157,6 +165,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = max(5, min(8, text.count / 14))
case let .actionSucceeded(title, text, cancel):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -174,6 +183,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
undoText = cancel
self.originalRemainingSeconds = 5
case let .chatAddedToFolder(chatTitle, folderTitle):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -190,6 +200,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 5
case let .chatRemovedFromFolder(chatTitle, folderTitle):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -206,6 +217,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 5
case let .messagesUnpinned(title, text, undo, isHidden):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: isHidden ? "anim_message_hidepin" : "anim_message_unpin", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
@ -223,6 +235,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = undo
self.originalRemainingSeconds = 5
case let .emoji(path, text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
@ -238,6 +251,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 5
case let .swipeToReply(title, text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: "anim_swipereply", colors: [:], scale: 1.0)
@ -248,6 +262,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false
self.originalRemainingSeconds = 5
case let .stickersModified(title, text, undo, info, topItem, account):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
@ -335,6 +350,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
}
}
case let .dice(dice, account, text, action):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
@ -389,6 +405,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
})
}
case let .setProximityAlert(title, text, cancelled):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: cancelled ? "anim_proximity_cancelled" : "anim_proximity_set", colors: [:], scale: 0.45)
@ -403,6 +420,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = attributedText
}
displayUndo = false
self.originalRemainingSeconds = 3
case let .invitedToVoiceChat(context, peer, text):
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
self.animatedStickerNode = nil
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.avatarNode?.setPeer(context: context, theme: presentationData.theme, peer: peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: true)
displayUndo = false
self.originalRemainingSeconds = 3
}
@ -433,7 +467,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:
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat:
break
case .dice:
self.panelWrapperNode.clipsToBounds = true
@ -447,6 +481,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.stillStickerNode.flatMap(self.panelWrapperNode.addSubnode)
self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode)
self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode)
self.avatarNode.flatMap(self.panelWrapperNode.addSubnode)
self.panelWrapperNode.addSubnode(self.titleNode)
self.panelWrapperNode.addSubnode(self.textNode)
self.panelWrapperNode.addSubnode(self.buttonNode)
@ -605,9 +640,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
}
let textContentOrigin = floor((contentHeight - textContentHeight) / 2.0)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin + textOffset), size: textSize))
if let iconNode = self.iconNode, let iconSize = iconNode.image?.size {
@ -660,16 +693,22 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize)
transition.updateFrame(node: slotMachineNode, frame: iconFrame)
}
let timerTextSize = self.timerTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: self.timerTextNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - timerTextSize.width) / 2.0), y: floor((contentHeight - timerTextSize.height) / 2.0)), size: timerTextSize))
let statusSize: CGFloat = 30.0
if let statusNode = self.statusNode {
let statusSize: CGFloat = 30.0
transition.updateFrame(node: statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize)))
if firstLayout {
statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {})
}
}
if let avatarNode = self.avatarNode {
let avatarSize: CGFloat = 30.0
transition.updateFrame(node: avatarNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - avatarSize) / 2.0), y: floor((contentHeight - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
}
}
func animateIn(asReplacement: Bool) {