Merge commit 'e2e63a4ddfb52d7d97132cacb9384b7a2c628420'

This commit is contained in:
Ali 2021-03-24 02:27:19 +04:00
commit 50fe822e85
26 changed files with 6190 additions and 5730 deletions

View File

@ -6309,3 +6309,11 @@ Sorry for the inconvenience.";
"UserInfo.LinkForwardTooltip.TwoChats.One" = "Link forwarded to **%@** and **%@**";
"UserInfo.LinkForwardTooltip.ManyChats.One" = "Link forwarded to **%@** and %@ others";
"UserInfo.LinkForwardTooltip.SavedMessages.One" = "Link forwarded to **Saved Messages**";
"VoiceChat.You" = "this is you";
"VoiceChat.ChangePhoto" = "Change Photo";
"VoiceChat.EditBio" = "Edit Bio";
"VoiceChat.EditBioTitle" = "Bio";
"VoiceChat.EditBioText" = "Any details such as age, occupation or city.";
"VoiceChat.EditBioPlaceholder" = "Bio";
"VoiceChat.EditBioSave" = "Save";

View File

@ -1625,7 +1625,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if case let .extracted(source) = self.source {
if !source.ignoreContentTouches {
let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view)
if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) {
if let result = contentParentNode.contentNode.customHitTest?(contentPoint) {
return result
} else if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) {
if result is TextSelectionNodeView {
return result
} else if contentParentNode.contentRect.contains(contentPoint) {

View File

@ -26,6 +26,7 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode {
}
public final class ContextExtractedContentNode: ASDisplayNode {
public var customHitTest: ((CGPoint) -> UIView?)?
}
public final class ContextControllerContentNode: ASDisplayNode {

View File

@ -424,6 +424,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
@objc private func seekBackwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
self.interacting?(true)
self.backwardButton.isPressing = true
self.wasPlaying = !self.currentIsPaused
if self.wasPlaying == true {
@ -447,6 +448,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.seekTimer = seekTimer
seekTimer.start()
case .ended, .cancelled:
self.interacting?(false)
self.backwardButton.isPressing = false
self.seekTimer?.invalidate()
self.seekTimer = nil
@ -462,6 +464,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
@objc private func seekForwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
self.interacting?(true)
self.forwardButton.isPressing = true
self.wasPlaying = !self.currentIsPaused
if self.wasPlaying == false {
@ -485,6 +488,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.seekTimer = seekTimer
seekTimer.start()
case .ended, .cancelled:
self.interacting?(false)
self.forwardButton.isPressing = false
self.setPlayRate?(1.0)
self.seekTimer?.invalidate()
@ -1372,11 +1376,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
@objc func backwardButtonPressed() {
self.interacting?(true)
self.seekBackward?(15.0)
self.interacting?(false)
}
@objc func forwardButtonPressed() {
self.interacting?(true)
self.seekForward?(15.0)
self.interacting?(false)
}
@objc private func statusPressed() {
@ -1540,7 +1548,7 @@ private final class PlaybackButtonNode: HighlightTrackingButtonNode {
strongSelf.textNode.alpha = 1.0
strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .linear)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: 0.0)
}
}

View File

@ -345,20 +345,26 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
self.scrubberView.updateScrubbing = { [weak self] timecode in
guard let strongSelf = self, let videoFramePreview = strongSelf.videoFramePreview else {
guard let strongSelf = self else {
return
}
if let timecode = timecode {
if !strongSelf.scrubbingFrames {
strongSelf.scrubbingFrames = true
strongSelf.scrubbingFrame.set(videoFramePreview.generatedFrames
|> map(Optional.init))
strongSelf.isInteractingPromise.set(timecode != nil)
if let videoFramePreview = strongSelf.videoFramePreview {
if let timecode = timecode {
if !strongSelf.scrubbingFrames {
strongSelf.scrubbingFrames = true
strongSelf.scrubbingFrame.set(videoFramePreview.generatedFrames
|> map(Optional.init))
}
videoFramePreview.generateFrame(at: timecode)
} else {
strongSelf.isInteractingPromise.set(false)
strongSelf.scrubbingFrame.set(.single(nil))
videoFramePreview.cancelPendingFrames()
strongSelf.scrubbingFrames = false
}
videoFramePreview.generateFrame(at: timecode)
} else {
strongSelf.scrubbingFrame.set(.single(nil))
videoFramePreview.cancelPendingFrames()
strongSelf.scrubbingFrames = false
}
}

View File

@ -577,7 +577,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus {
return .single(value) |> delay(0.25, queue: Queue.concurrentDefaultQueue())
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
} else {
return .single(value)
}

View File

@ -5,9 +5,8 @@ import Postbox
import TelegramCore
import SyncCore
import FFMpegBinding
import UniversalMediaPlayer
func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceReference, duration: Double) -> Signal<Never, NoError> {
public func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceReference, duration: Double) -> Signal<Never, NoError> {
return Signal { subscriber in
let queue = Queue()
let disposable = MetaDisposable()

View File

@ -22,6 +22,40 @@ public enum AvatarGalleryEntryId: Hashable {
case resource(String)
}
public func peerInfoProfilePhotos(context: AccountContext, peerId: PeerId) -> Signal<Any, NoError> {
return context.account.postbox.combinedView(keys: [.basicPeer(peerId)])
|> mapToSignal { view -> Signal<AvatarGalleryEntry?, NoError> in
guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else {
return .single(nil)
}
return initialAvatarGalleryEntries(account: context.account, peer: peer)
|> map { entries in
return entries.first
}
}
|> distinctUntilChanged
|> mapToSignal { firstEntry -> Signal<(Bool, [AvatarGalleryEntry]), NoError> in
if let firstEntry = firstEntry {
return context.account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<(Bool, [AvatarGalleryEntry]), NoError>in
return fetchedAvatarGalleryEntries(account: context.account, peer: peer, firstEntry: firstEntry)
}
} else {
return .single((true, []))
}
}
|> map { items -> Any in
return items
}
}
public func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> {
return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId))
|> map { items -> (Bool, [AvatarGalleryEntry]) in
return items as? (Bool, [AvatarGalleryEntry]) ?? (true, [])
}
}
public enum AvatarGalleryEntry: Equatable {
case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, GalleryItemIndexData?, Data?, String?)
case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, Int32?, GalleryItemIndexData?, MessageId?, Data?, String?)
@ -115,20 +149,20 @@ public final class AvatarGalleryControllerPresentationArguments {
}
public func normalizeEntries(_ entries: [AvatarGalleryEntry]) -> [AvatarGalleryEntry] {
var updatedEntries: [AvatarGalleryEntry] = []
let count: Int32 = Int32(entries.count)
var index: Int32 = 0
for entry in entries {
let indexData = GalleryItemIndexData(position: index, totalCount: count)
if case let .topImage(representations, videoRepresentations, peer, _, immediateThumbnailData, category) = entry {
updatedEntries.append(.topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category))
} else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category) = entry {
updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category))
}
index += 1
var updatedEntries: [AvatarGalleryEntry] = []
let count: Int32 = Int32(entries.count)
var index: Int32 = 0
for entry in entries {
let indexData = GalleryItemIndexData(position: index, totalCount: count)
if case let .topImage(representations, videoRepresentations, peer, _, immediateThumbnailData, category) = entry {
updatedEntries.append(.topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category))
} else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category) = entry {
updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category))
}
return updatedEntries
index += 1
}
return updatedEntries
}
public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> {
var initialEntries: [AvatarGalleryEntry] = []

View File

@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PeerInfoAvatarListNode",
module_name = "PeerInfoAvatarListNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AvatarNode:AvatarNode",
"//submodules/PhotoResources:PhotoResources",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/GalleryUI:GalleryUI",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/AccountContext:AccountContext",
],
visibility = [
"//visibility:public",
],
)

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,8 @@ swift_library(
"//submodules/DeviceProximity:DeviceProximity",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
"//submodules/WebSearchUI:WebSearchUI",
],
visibility = [
"//visibility:public",

View File

@ -24,6 +24,10 @@ import DirectionalPanGesture
import PeerInfoUI
import AvatarNode
import TooltipUI
import LegacyUI
import LegacyComponents
import LegacyMediaPickerUI
import WebSearchUI
private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
@ -598,7 +602,9 @@ public final class VoiceChatController: ViewController {
}
switch state {
case .listening:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
if peerEntry.isMyPeer {
text = .text(presentationData.strings.VoiceChat_You, .accent)
} else if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, .destructive)
} else if let about = peerEntry.about, !about.isEmpty {
text = .text(about, .generic)
@ -629,7 +635,9 @@ public final class VoiceChatController: ViewController {
text = .text(presentationData.strings.VoiceChat_StatusInvited, .generic)
icon = .invite(true)
case .raisedHand:
if let about = peerEntry.about, !about.isEmpty && !peerEntry.displayRaisedHandStatus {
if peerEntry.isMyPeer && !peerEntry.displayRaisedHandStatus {
text = .text(presentationData.strings.VoiceChat_You, .accent)
} else if let about = peerEntry.about, !about.isEmpty && !peerEntry.displayRaisedHandStatus {
text = .text(about, .generic)
} else {
text = .text(presentationData.strings.VoiceChat_StatusWantsToSpeak, .accent)
@ -668,6 +676,8 @@ public final class VoiceChatController: ViewController {
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated)
}
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
private weak var controller: VoiceChatController?
private let sharedContext: SharedAccountContext
private let context: AccountContext
@ -1221,6 +1231,35 @@ public final class VoiceChatController: ViewController {
f(.default)
})))
}
// items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ChangePhoto, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor)
// }, action: { _, f in
// guard let strongSelf = self else {
// return
// }
//
// f(.default)
//
// strongSelf.openAvatarForEditing(fromGallery: false, completion: {})
// })))
// items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditBio, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
// }, action: { _, f in
// guard let strongSelf = self else {
// return
// }
// f(.default)
//
// let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, apply: { bio in
// if let strongSelf = self {
// let _ = updateAbout(account: strongSelf.context.account, about: bio)
// |> `catch` { _ -> Signal<Void, NoError> in
// return .complete()
// }
// }
// })
// self?.controller?.present(controller, in: .window(.root))
// })))
} else {
if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) {
if callState.adminIds.contains(peer.id) {
@ -3481,6 +3520,152 @@ public final class VoiceChatController: ViewController {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
guard let peerId = self.callState?.myPeerId else {
return
}
let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in
return (transaction.getPeer(peerId), currentSearchBotsConfiguration(transaction: transaction))
}
|> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in
guard let strongSelf = self, let peer = peer else {
return
}
let presentationData = strongSelf.presentationData
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
legacyController.bind(controller: navigationController)
strongSelf.view.endEditing(true)
strongSelf.controller?.present(legacyController, in: .window(.root))
var hasPhotos = false
if !peer.profileImageRepresentations.isEmpty {
hasPhotos = true
}
let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context)
// paintStickersContext.presentStickersController = { completion in
// let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
// let coder = PostboxEncoder()
// coder.encodeRootObject(fileReference.media)
// completion?(coder.makeData(), fileReference.media.isAnimatedSticker, node.view, rect)
// return true
// })
// strongSelf.controller?.present(controller, in: .window(.root))
// return controller
// }
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
mixin.stickersContext = paintStickersContext
let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
guard let strongSelf = self else {
return
}
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
assetsController?.dismiss()
// self?.updateProfilePhoto(result)
}))
controller.navigationPresentation = .modal
strongSelf.controller?.push(controller)
if fromGallery {
completion()
}
}
mixin.didFinishWithImage = { [weak self] image in
if let image = image {
completion()
// self?.updateProfilePhoto(image)
}
}
mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in
if let image = image, let asset = asset {
completion()
// self?.updateProfileVideo(image, asset: asset, adjustments: adjustments)
}
}
mixin.didFinishWithDelete = {
guard let strongSelf = self else {
return
}
// let proceed = {
// if let item = item {
// strongSelf.deleteAvatar(item, remove: false)
// }
//
// let _ = strongSelf.currentAvatarMixin.swap(nil)
// if let _ = peer.smallProfileImage {
// strongSelf.state = strongSelf.state.withUpdatingAvatar(nil)
// if let (layout, navigationHeight) = strongSelf.validLayout {
// strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
// }
// }
// let postbox = strongSelf.context.account.postbox
// strongSelf.updateAvatarDisposable.set((updatePeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, accountPeerId: strongSelf.context.account.peerId, peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
// return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
// })
// |> deliverOnMainQueue).start(next: { result in
// guard let strongSelf = self else {
// return
// }
// switch result {
// case .complete:
// strongSelf.state = strongSelf.state.withUpdatingAvatar(nil)
// if let (layout, navigationHeight) = strongSelf.validLayout {
// strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
// }
// case .progress:
// break
// }
// }))
// }
//
// let actionSheet = ActionSheetController(presentationData: presentationData)
// let items: [ActionSheetItem] = [
// ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// proceed()
// })
// ]
//
// actionSheet.setItemGroups([
// ActionSheetItemGroup(items: items),
// ActionSheetItemGroup(items: [
// ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// })
// ])
// ])
// strongSelf.controller?.present(actionSheet, in: .window(.root))
}
mixin.didDismiss = { [weak legacyController] in
guard let strongSelf = self else {
return
}
let _ = strongSelf.currentAvatarMixin.swap(nil)
legacyController?.dismiss()
}
let menuController = mixin.present()
if let menuController = menuController {
menuController.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
}
})
}
}
private let sharedContext: SharedAccountContext
@ -3521,6 +3706,8 @@ public final class VoiceChatController: ViewController {
super.init(navigationBarPresentationData: nil)
self.blocksBackgroundWhenInOverlay = true
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .Ignore
@ -3698,7 +3885,7 @@ public final class VoiceChatController: ViewController {
private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
let ignoreContentTouches: Bool = false
let blurBackground: Bool
private let controller: ViewController

View File

@ -17,6 +17,7 @@ import ContextUI
import AccountContext
import LegacyComponents
import AudioBlob
import PeerInfoAvatarListNode
final class VoiceChatParticipantItem: ListViewItem {
enum ParticipantText {
@ -158,6 +159,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private var extractedVerticalOffset: CGFloat?
fileprivate let avatarNode: AvatarNode
private let titleNode: TextNode
@ -165,6 +167,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private let expandedStatusNode: TextNode
private var credibilityIconNode: ASImageNode?
private var avatarTransitionNode: ASImageNode?
private var avatarListContainerNode: ASDisplayNode?
private var avatarListWrapperNode: ASDisplayNode?
private var avatarListNode: PeerInfoAvatarListContainerNode?
private let actionContainerNode: ASDisplayNode
private var animationNode: VoiceChatMicrophoneNode?
private var iconNode: ASImageNode?
@ -201,6 +208,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.clipsToBounds = true
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
@ -282,29 +290,187 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
strongSelf.isExtracted = isExtracted
let inset: CGFloat = 12.0
let cornerRadius: CGFloat = 14.0
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.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0)
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
strongSelf.contextSourceNode.contentNode.customHitTest = { [weak self] point in
if let strongSelf = self {
if let avatarListContainerNode = strongSelf.avatarListContainerNode, avatarListContainerNode.frame.contains(point) {
return strongSelf.avatarListNode?.view
}
}
return nil
}
})
} else {
strongSelf.contextSourceNode.contentNode.customHitTest = nil
}
let extractedVerticalOffset = strongSelf.extractedVerticalOffset ?? 0.0
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect: CGRect
if isExtracted {
if extractedVerticalOffset > 0.0 {
rect = CGRect(x: extractedRect.minX, y: extractedRect.minY + extractedVerticalOffset, width: extractedRect.width, height: extractedRect.height - extractedVerticalOffset)
} else {
rect = extractedRect
}
} else {
rect = nonExtractedRect
}
let springDuration: Double = isExtracted ? 0.42 : 0.3
let springDamping: CGFloat = isExtracted ? 104.0 : 1000.0
if !extractedVerticalOffset.isZero {
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateImage(CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(item.presentationData.theme.list.itemBlocksBackgroundColor.cgColor)
context.fillEllipse(in: bounds)
context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
strongSelf.extractedBackgroundImageNode.cornerRadius = cornerRadius
var avatarInitialRect = strongSelf.avatarNode.view.convert(strongSelf.avatarNode.bounds, to: strongSelf.offsetContainerNode.supernode?.view)
if strongSelf.avatarTransitionNode == nil {
transition.updateCornerRadius(node: strongSelf.extractedBackgroundImageNode, cornerRadius: 0.0)
let targetRect = CGRect(x: extractedRect.minX, y: extractedRect.minY, width: extractedRect.width, height: extractedRect.width)
let initialScale = avatarInitialRect.width / targetRect.width
avatarInitialRect.origin.y += cornerRadius / 2.0 * initialScale
let avatarListWrapperNode = ASDisplayNode()
avatarListWrapperNode.clipsToBounds = true
avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.height + cornerRadius)
avatarListWrapperNode.cornerRadius = cornerRadius
let transitionNode = ASImageNode()
transitionNode.clipsToBounds = true
transitionNode.displaysAsynchronously = false
transitionNode.displayWithoutProcessing = true
transitionNode.image = strongSelf.avatarNode.unroundedImage
transitionNode.frame = CGRect(origin: CGPoint(), size: targetRect.size)
transitionNode.cornerRadius = targetRect.width / 2.0
transition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0)
strongSelf.avatarNode.isHidden = true
avatarListWrapperNode.addSubnode(transitionNode)
strongSelf.avatarTransitionNode = transitionNode
let avatarListContainerNode = ASDisplayNode()
avatarListContainerNode.clipsToBounds = true
avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetRect.size)
avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
avatarListContainerNode.cornerRadius = targetRect.width / 2.0
avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarInitialRect.center), to: NSValue(cgPoint: avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
transition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: 0.0)
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
avatarListNode.peer = item.peer
avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.height / 2.0, width: targetRect.width, height: targetRect.height)
avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.height / 2.0, width: targetRect.width, height: targetRect.height)
avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.height / 2.0), size: CGSize())
avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0)
avatarListContainerNode.addSubnode(avatarListNode)
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
avatarListWrapperNode.addSubnode(avatarListContainerNode)
avatarListNode.update(size: targetRect.size, peer: item.peer, isExpanded: true, transition: .immediate)
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
strongSelf.avatarListWrapperNode = avatarListWrapperNode
strongSelf.avatarListContainerNode = avatarListContainerNode
strongSelf.avatarListNode = avatarListNode
}
} else if let transitionNode = strongSelf.avatarTransitionNode, let avatarListWrapperNode = strongSelf.avatarListWrapperNode, let avatarListContainerNode = strongSelf.avatarListContainerNode {
transition.updateCornerRadius(node: strongSelf.extractedBackgroundImageNode, cornerRadius: cornerRadius)
var avatarInitialRect = CGRect(origin: strongSelf.avatarNode.frame.origin, size: strongSelf.avatarNode.frame.size)
let targetScale = avatarInitialRect.width / avatarListContainerNode.frame.width
avatarInitialRect.origin.y += cornerRadius / 2.0 * targetScale
strongSelf.avatarTransitionNode = nil
strongSelf.avatarListWrapperNode = nil
strongSelf.avatarListContainerNode = nil
strongSelf.avatarListNode = nil
avatarListContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak avatarListContainerNode] _ in
avatarListContainerNode?.removeFromSupernode()
})
avatarListWrapperNode.layer.animateSpring(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false)
avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarListWrapperNode.position), to: NSValue(cgPoint: avatarInitialRect.center), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false, completion: { [weak transitionNode, weak self] _ in
transitionNode?.removeFromSupernode()
self?.avatarNode.isHidden = false
})
transition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: avatarListContainerNode.frame.width / 2.0)
transition.updateCornerRadius(node: transitionNode, cornerRadius: avatarListContainerNode.frame.width / 2.0)
}
transition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0)
let offsetInitialSublayerTransform = strongSelf.offsetContainerNode.layer.sublayerTransform
strongSelf.offsetContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? -33 : 0.0, isExtracted ? extractedVerticalOffset : 0.0, 0.0)
let actionInitialSublayerTransform = strongSelf.actionContainerNode.layer.sublayerTransform
strongSelf.actionContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? 21.0 : 0.0, 0.0, 0.0)
let extractedInitialBackgroundPosition = strongSelf.extractedBackgroundImageNode.position
strongSelf.extractedBackgroundImageNode.layer.position = rect.center
let extractedInitialBackgroundBounds = strongSelf.extractedBackgroundImageNode.bounds
strongSelf.extractedBackgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: rect.size)
if isExtracted {
strongSelf.offsetContainerNode.layer.animateSpring(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping)
strongSelf.actionContainerNode.layer.animateSpring(from: NSValue(caTransform3D: actionInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.actionContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping)
strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: extractedInitialBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping)
strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgRect: extractedInitialBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
} else {
strongSelf.offsetContainerNode.layer.animate(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.25)
strongSelf.actionContainerNode.layer.animate(from: NSValue(caTransform3D: actionInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.actionContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.25)
strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgPoint: extractedInitialBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.25)
strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgRect: extractedInitialBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.25)
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.alpha = 1.0
strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.06, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
} else {
strongSelf.extractedBackgroundImageNode.alpha = 0.0
strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.15, removeOnCompletion: false, completion: { [weak self] _ in
self?.extractedBackgroundImageNode.image = nil
self?.extractedBackgroundImageNode.layer.removeAllAnimations()
})
}
} else {
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius * 2.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor)
}
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
transition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? inset : 0.0, y: isExtracted ? extractedVerticalOffset : 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
}
})
}
}
}
}
@ -312,7 +478,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
self.audioLevelDisposable.dispose()
self.raiseHandTimer?.invalidate()
}
override func selected() {
super.selected()
self.layoutParams?.0.action?(self.contextSourceNode)
@ -515,12 +681,19 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
strongSelf.layoutParams = (item, params, first, last)
strongSelf.wavesColor = wavesColor
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height))
let nonExtractedRect = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: layout.contentSize.width - 32.0, height: layout.contentSize.height))
var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
let extractedHeight = extractedRect.height + expandedStatusLayout.size.height - statusLayout.size.height
var extractedHeight = extractedRect.height + expandedStatusLayout.size.height - statusLayout.size.height
var extractedVerticalOffset: CGFloat = 0.0
if item.peer.smallProfileImage != nil {
extractedVerticalOffset = extractedRect.width
extractedHeight += extractedVerticalOffset
}
extractedRect.size.height = extractedHeight
strongSelf.extractedVerticalOffset = extractedVerticalOffset
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
@ -702,7 +875,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
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.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad, storeUnrounded: true)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))

View File

@ -408,7 +408,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode {
}
}
func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, value: String?, apply: @escaping (String?) -> Void) -> AlertController {
func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, apply: @escaping (String?) -> Void) -> AlertController {
var presentationData = sharedContext.currentPresentationData.with { $0 }
if let forceTheme = forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
@ -419,7 +419,7 @@ func voiceChatTitleEditController(sharedContext: SharedAccountContext, account:
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
}), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: {
applyImpl?()
})]

View File

@ -218,6 +218,7 @@ swift_library(
"//submodules/DatePickerNode:DatePickerNode",
"//submodules/ConfettiEffect:ConfettiEffect",
"//submodules/Speak:Speak",
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -64,6 +64,7 @@ import ChatHistoryImportTasks
import Markdown
import TelegramPermissionsUI
import Speak
import UniversalMediaPlayer
extension ChatLocation {
var peerId: PeerId {
@ -1627,7 +1628,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied)
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}))
}
@ -1652,7 +1653,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied)
let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied)
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})
]), ActionSheetItemGroup(items: [

View File

@ -6,6 +6,7 @@ import SwiftSignalKit
import Postbox
import TelegramUIPreferences
import AccountContext
import UniversalMediaPlayer
private struct FetchManagerLocationEntryId: Hashable {
let location: FetchManagerLocation

View File

@ -6,6 +6,7 @@ import SyncCore
import TelegramUIPreferences
import AccountContext
import PhotoResources
import UniversalMediaPlayer
private final class PrefetchMediaContext {
let fetchDisposable = MetaDisposable()

View File

@ -338,40 +338,6 @@ private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId, is
|> distinctUntilChanged
}
private func peerInfoProfilePhotos(context: AccountContext, peerId: PeerId) -> Signal<Any, NoError> {
return context.account.postbox.combinedView(keys: [.basicPeer(peerId)])
|> mapToSignal { view -> Signal<AvatarGalleryEntry?, NoError> in
guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else {
return .single(nil)
}
return initialAvatarGalleryEntries(account: context.account, peer: peer)
|> map { entries in
return entries.first
}
}
|> distinctUntilChanged
|> mapToSignal { firstEntry -> Signal<(Bool, [AvatarGalleryEntry]), NoError> in
if let firstEntry = firstEntry {
return context.account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<(Bool, [AvatarGalleryEntry]), NoError>in
return fetchedAvatarGalleryEntries(account: context.account, peer: peer, firstEntry: firstEntry)
}
} else {
return .single((true, []))
}
}
|> map { items -> Any in
return items
}
}
func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> {
return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId))
|> map { items -> (Bool, [AvatarGalleryEntry]) in
return items as? (Bool, [AvatarGalleryEntry]) ?? (true, [])
}
}
func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signal<Never, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: false)
|> mapToSignal { inputData -> Signal<Never, NoError> in

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,7 @@ import MediaResources
import HashtagSearchUI
import ActionSheetPeerItem
import TelegramCallsUI
import PeerInfoAvatarListNode
protocol PeerInfoScreenItem: class {
var id: AnyHashable { get }

View File

@ -7,6 +7,7 @@ import TelegramUIPreferences
import AccountContext
import PhotoResources
import Emoji
import UniversalMediaPlayer
private final class PrefetchMediaContext {
let fetchDisposable = MetaDisposable()