mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit 'ce883e2d24cc58f728e08c73bb77a1650e99b648'
This commit is contained in:
commit
fe9f5c5014
@ -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";
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -22,7 +22,6 @@ swift_library(
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AudioBlob:AudioBlob",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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 {
|
||||
|
@ -14,6 +14,4 @@
|
||||
- (TGMediaAsset *)assetAtIndex:(NSUInteger)index;
|
||||
- (NSUInteger)indexOfAsset:(TGMediaAsset *)asset;
|
||||
|
||||
- (NSSet *)itemsIdentifiers;
|
||||
|
||||
@end
|
||||
|
@ -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
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 []
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)?
|
||||
|
@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
823
submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift
Normal file
823
submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
200
submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift
Normal file
200
submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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?()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
12
submodules/TelegramUI/Images.xcassets/Call/Context Menu/Invited.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/Context Menu/Invited.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_invited.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Voice.pdf",
|
||||
"filename" : "ic_mute.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
BIN
submodules/TelegramUI/Images.xcassets/Call/Context Menu/Mute.imageset/ic_mute.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/Context Menu/Mute.imageset/ic_mute.pdf
vendored
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Voice.pdf",
|
||||
"filename" : "ic_unmute.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
BIN
submodules/TelegramUI/Images.xcassets/Call/Context Menu/Unmute.imageset/ic_unmute.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/Context Menu/Unmute.imageset/ic_unmute.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Check.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Check.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_menucheck.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
@ -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":[]}
|
Binary file not shown.
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ swift_library(
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user