mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Temporary
This commit is contained in:
parent
65b22f6ca5
commit
d6188e2124
@ -567,9 +567,9 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo
|
||||
completion(.success(with: .missed))
|
||||
}
|
||||
|
||||
public func resolveCallType(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeResolutionResult) -> Void) {
|
||||
/*public func resolveCallType(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeResolutionResult) -> Void) {
|
||||
completion(.success(with: .missed))
|
||||
}
|
||||
}*/
|
||||
|
||||
public func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|
83
buildbox/deploy-appcenter.sh
Normal file
83
buildbox/deploy-appcenter.sh
Normal file
@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
API_HOST="https://api.appcenter.ms"
|
||||
IPA_PATH="build/Telegram_Signed.ipa"
|
||||
DSYM_PATH="build/DSYM.zip"
|
||||
|
||||
upload_ipa() {
|
||||
GROUP_DATA=$(curl \
|
||||
-X GET \
|
||||
--header "X-API-Token: $API_TOKEN" \
|
||||
"$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/distribution_groups/Internal" \
|
||||
)
|
||||
|
||||
GROUP_ID=$(echo "$GROUP_DATA" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["id"];')
|
||||
|
||||
UPLOAD_TOKEN=$(curl \
|
||||
-X POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--header "X-API-Token: $API_TOKEN" \
|
||||
"$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/release_uploads" \
|
||||
)
|
||||
|
||||
|
||||
UPLOAD_URL=$(echo "$UPLOAD_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["upload_url"];')
|
||||
UPLOAD_ID=$(echo "$UPLOAD_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["upload_id"];')
|
||||
|
||||
curl --progress-bar -F "ipa=@${IPA_PATH}" "$UPLOAD_URL"
|
||||
|
||||
RELEASE_TOKEN=$(curl \
|
||||
-X PATCH \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--header "X-API-Token: $API_TOKEN" \
|
||||
-d '{ "status": "committed" }' \
|
||||
"$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/release_uploads/$UPLOAD_ID" \
|
||||
)
|
||||
|
||||
|
||||
RELEASE_URL=$(echo "$RELEASE_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["release_url"];')
|
||||
RELEASE_ID=$(echo "$RELEASE_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["release_id"];')
|
||||
|
||||
curl \
|
||||
-X POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--header "X-API-Token: $API_TOKEN" \
|
||||
-d "{ \"id\": \"$GROUP_ID\", \"mandatory_update\": false, \"notify_testers\": false }" \
|
||||
"$API_HOST/$RELEASE_URL/groups"
|
||||
}
|
||||
|
||||
upload_dsym() {
|
||||
UPLOAD_DSYM_DATA=$(curl \
|
||||
-X POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--header "X-API-Token: $API_TOKEN" \
|
||||
-d "{ \"symbol_type\": \"Apple\"}" \
|
||||
"$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/symbol_uploads" \
|
||||
)
|
||||
|
||||
DSYM_UPLOAD_URL=$(echo "$UPLOAD_DSYM_DATA" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["upload_url"];')
|
||||
DSYM_UPLOAD_ID=$(echo "$UPLOAD_DSYM_DATA" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["symbol_upload_id"];')
|
||||
|
||||
curl \
|
||||
--progress-bar \
|
||||
--header "x-ms-blob-type: BlockBlob" \
|
||||
--upload-file "${DSYM_PATH}" \
|
||||
"$DSYM_UPLOAD_URL"
|
||||
|
||||
curl \
|
||||
-X PATCH \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--header "X-API-Token: $API_TOKEN" \
|
||||
-d '{ "status": "committed" }' \
|
||||
"$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/symbol_uploads/$DSYM_UPLOAD_ID"
|
||||
}
|
||||
|
||||
upload_ipa
|
@ -34,12 +34,10 @@ fi
|
||||
if [ "$CONFIGURATION" == "hockeyapp" ]; then
|
||||
FASTLANE_PASSWORD=""
|
||||
FASTLANE_ITC_TEAM_NAME=""
|
||||
FASTLANE_BUILD_CONFIGURATION="internalhockeyapp"
|
||||
elif [ "$CONFIGURATION" == "appstore" ]; then
|
||||
FASTLANE_PASSWORD="$TELEGRAM_BUILD_APPSTORE_PASSWORD"
|
||||
FASTLANE_ITC_TEAM_NAME="$TELEGRAM_BUILD_APPSTORE_TEAM_NAME"
|
||||
FASTLANE_ITC_USERNAME="$TELEGRAM_BUILD_APPSTORE_USERNAME"
|
||||
FASTLANE_BUILD_CONFIGURATION="testflight_llc"
|
||||
else
|
||||
echo "Unknown configuration $CONFIGURATION"
|
||||
exit 1
|
||||
@ -62,7 +60,6 @@ fi
|
||||
if [ "$1" == "appstore" ]; then
|
||||
export DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS="-t DAV"
|
||||
FASTLANE_PASSWORD="$FASTLANE_PASSWORD" xcrun altool --upload-app --type ios --file "$IPA_PATH" --username "$FASTLANE_ITC_USERNAME" --password "@env:FASTLANE_PASSWORD"
|
||||
#FASTLANE_PASSWORD="$FASTLANE_PASSWORD" FASTLANE_ITC_TEAM_NAME="$FASTLANE_ITC_TEAM_NAME" fastlane "$FASTLANE_BUILD_CONFIGURATION" build_number:"$BUILD_NUMBER" commit_hash:"$COMMIT_ID" commit_author:"$COMMIT_AUTHOR" skip_build:1 skip_pilot:1
|
||||
else
|
||||
FASTLANE_PASSWORD="$FASTLANE_PASSWORD" FASTLANE_ITC_TEAM_NAME="$FASTLANE_ITC_TEAM_NAME" fastlane "$FASTLANE_BUILD_CONFIGURATION" build_number:"$BUILD_NUMBER" commit_hash:"$COMMIT_ID" commit_author:"$COMMIT_AUTHOR" skip_build:1
|
||||
else if [ "$1" == "hockeyapp" ]; then
|
||||
API_USER_NAME="$API_USER_NAME" API_APP_NAME="$API_APP_NAME" API_TOKEN="$API_TOKEN" sh buildbox/deploy-appcenter.sh
|
||||
fi
|
||||
|
@ -197,12 +197,13 @@ public final class NavigateToChatControllerParams {
|
||||
public let keepStack: NavigateToChatKeepStack
|
||||
public let purposefulAction: (() -> Void)?
|
||||
public let scrollToEndIfExists: Bool
|
||||
public let activateMessageSearch: Bool
|
||||
public let animated: Bool
|
||||
public let options: NavigationAnimationOptions
|
||||
public let parentGroupId: PeerGroupId?
|
||||
public let completion: () -> Void
|
||||
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping () -> Void = {}) {
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: Bool = false, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping () -> Void = {}) {
|
||||
self.navigationController = navigationController
|
||||
self.chatController = chatController
|
||||
self.context = context
|
||||
@ -214,6 +215,7 @@ public final class NavigateToChatControllerParams {
|
||||
self.keepStack = keepStack
|
||||
self.purposefulAction = purposefulAction
|
||||
self.scrollToEndIfExists = scrollToEndIfExists
|
||||
self.activateMessageSearch = activateMessageSearch
|
||||
self.animated = animated
|
||||
self.options = options
|
||||
self.parentGroupId = parentGroupId
|
||||
|
@ -191,6 +191,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
private let imageReadyDisposable = MetaDisposable()
|
||||
private var state: AvatarNodeState = .empty
|
||||
|
||||
public var unroundedImage: UIImage?
|
||||
|
||||
private let imageReady = Promise<Bool>(false)
|
||||
public var ready: Signal<Void, NoError> {
|
||||
let imageReady = self.imageReady
|
||||
@ -283,7 +285,7 @@ public final class AvatarNode: ASDisplayNode {
|
||||
self.imageNode.isHidden = true
|
||||
}
|
||||
|
||||
public func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) {
|
||||
public func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) {
|
||||
var synchronousLoad = synchronousLoad
|
||||
var representation: TelegramMediaImageRepresentation?
|
||||
var icon = AvatarNodeIcon.none
|
||||
@ -318,11 +320,18 @@ public final class AvatarNode: ASDisplayNode {
|
||||
|
||||
let parameters: AvatarNodeParameters
|
||||
|
||||
if let peer = peer, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, emptyColor: emptyColor, synchronousLoad: synchronousLoad) {
|
||||
if let peer = peer, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) {
|
||||
self.contents = nil
|
||||
self.displaySuspended = true
|
||||
self.imageReady.set(self.imageNode.ready)
|
||||
self.imageNode.setSignal(signal)
|
||||
self.imageNode.setSignal(signal |> beforeNext { [weak self] next in
|
||||
Queue.mainQueue().async {
|
||||
self?.unroundedImage = next?.1
|
||||
}
|
||||
}
|
||||
|> map { next -> UIImage? in
|
||||
return next?.0
|
||||
})
|
||||
|
||||
if case .editAvatarIcon = icon {
|
||||
if self.editOverlayNode == nil {
|
||||
|
@ -64,15 +64,15 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?,
|
||||
}
|
||||
}
|
||||
|
||||
public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false) -> Signal<UIImage?, NoError>? {
|
||||
public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false) -> Signal<(UIImage, UIImage)?, NoError>? {
|
||||
if let imageData = peerAvatarImageData(account: account, peerReference: peerReference, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) {
|
||||
return imageData
|
||||
|> mapToSignal { data -> Signal<UIImage?, NoError> in
|
||||
let generate = deferred { () -> Signal<UIImage?, NoError> in
|
||||
|> mapToSignal { data -> Signal<(UIImage, UIImage)?, NoError> in
|
||||
let generate = deferred { () -> Signal<(UIImage, UIImage)?, NoError> in
|
||||
if emptyColor == nil && data == nil {
|
||||
return .single(nil)
|
||||
}
|
||||
return .single(generateImage(displayDimensions, contextGenerator: { size, context -> Void in
|
||||
let roundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in
|
||||
if let data = data {
|
||||
if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
|
||||
context.clear(CGRect(origin: CGPoint(), size: displayDimensions))
|
||||
@ -110,7 +110,41 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut
|
||||
context.fill(CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
let unroundedImage: UIImage?
|
||||
if provideUnrounded {
|
||||
unroundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in
|
||||
if let data = data {
|
||||
if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
|
||||
context.clear(CGRect(origin: CGPoint(), size: displayDimensions))
|
||||
context.setBlendMode(.copy)
|
||||
|
||||
context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
|
||||
} else {
|
||||
if let emptyColor = emptyColor {
|
||||
context.clear(CGRect(origin: CGPoint(), size: displayDimensions))
|
||||
context.setFillColor(emptyColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
|
||||
}
|
||||
}
|
||||
} else if let emptyColor = emptyColor {
|
||||
context.clear(CGRect(origin: CGPoint(), size: displayDimensions))
|
||||
context.setFillColor(emptyColor.cgColor)
|
||||
if round {
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
|
||||
} else {
|
||||
context.fill(CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
unroundedImage = roundedImage
|
||||
}
|
||||
if let roundedImage = roundedImage, let unroundedImage = unroundedImage {
|
||||
return .single((roundedImage, unroundedImage))
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
if synchronousLoad {
|
||||
return generate
|
||||
|
@ -5,6 +5,9 @@ private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool {
|
||||
if view.disablesInteractiveTransitionGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
if let disablesInteractiveTransitionGestureRecognizerNow = view.disablesInteractiveTransitionGestureRecognizerNow, disablesInteractiveTransitionGestureRecognizerNow() {
|
||||
return true
|
||||
}
|
||||
|
||||
if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) {
|
||||
return true
|
||||
|
@ -188,14 +188,14 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
bottomController.viewWillAppear(true)
|
||||
let bottomNode = bottomController.displayNode
|
||||
|
||||
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self, topNode: topNode, topNavigationBar: topController.navigationBar, bottomNode: bottomNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self] progress, transition, topFrame, bottomFrame in
|
||||
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, isInteractive: true, container: self, topNode: topNode, topNavigationBar: topController.navigationBar, bottomNode: bottomNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self, weak bottomController] progress, transition, topFrame, bottomFrame in
|
||||
if let strongSelf = self {
|
||||
if let top = strongSelf.state.top {
|
||||
strongSelf.syncKeyboard(leftEdge: top.value.displayNode.frame.minX, transition: transition)
|
||||
|
||||
var updatedStatusBarStyle = strongSelf.statusBarStyle
|
||||
if let childTransition = strongSelf.state.transition, childTransition.coordinator.progress >= 0.3 {
|
||||
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
|
||||
if let bottomController = bottomController, progress >= 0.3 {
|
||||
updatedStatusBarStyle = bottomController.statusBar.statusBarStyle
|
||||
} else {
|
||||
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
|
||||
}
|
||||
@ -355,8 +355,21 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
self.applyLayout(layout: updatedLayout, to: top, isMaster: true, transition: transition)
|
||||
if let childTransition = self.state.transition, childTransition.coordinator.progress >= 0.3 {
|
||||
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
|
||||
if let childTransition = self.state.transition, childTransition.coordinator.isInteractive {
|
||||
switch childTransition.type {
|
||||
case .push:
|
||||
if childTransition.coordinator.progress >= 0.3 {
|
||||
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
|
||||
} else {
|
||||
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
|
||||
}
|
||||
case .pop:
|
||||
if childTransition.coordinator.progress >= 0.3 {
|
||||
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
|
||||
} else {
|
||||
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
|
||||
}
|
||||
@ -399,7 +412,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
toValue.value.setIgnoreAppearanceMethodInvocations(false)
|
||||
|
||||
let topTransition = TopTransition(type: transitionType, previous: fromValue, coordinator: NavigationTransitionCoordinator(transition: mappedTransitionType, container: self, topNode: topController.displayNode, topNavigationBar: topController.navigationBar, bottomNode: bottomController.displayNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self] _, transition, topFrame, bottomFrame in
|
||||
let topTransition = TopTransition(type: transitionType, previous: fromValue, coordinator: NavigationTransitionCoordinator(transition: mappedTransitionType, isInteractive: false, container: self, topNode: topController.displayNode, topNavigationBar: topController.navigationBar, bottomNode: bottomController.displayNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self] _, transition, topFrame, bottomFrame in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
public var backPressed: () -> () = { }
|
||||
|
||||
public var userInfo: Any?
|
||||
public var makeCustomTransitionNode: ((NavigationBar) -> CustomNavigationTransitionNode?)?
|
||||
public var makeCustomTransitionNode: ((NavigationBar, Bool) -> CustomNavigationTransitionNode?)?
|
||||
|
||||
private var collapsed: Bool {
|
||||
get {
|
||||
|
@ -306,6 +306,13 @@ private final class NavigationButtonItemNode: ASTextNode {
|
||||
public final class NavigationButtonNode: ASDisplayNode {
|
||||
private var nodes: [NavigationButtonItemNode] = []
|
||||
|
||||
public var singleCustomNode: ASDisplayNode? {
|
||||
for node in self.nodes {
|
||||
return node.node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var pressed: (Int) -> () = { _ in }
|
||||
public var highlightChanged: (Int, Bool) -> () = { _, _ in }
|
||||
|
||||
|
@ -35,6 +35,7 @@ final class NavigationTransitionCoordinator {
|
||||
|
||||
private let container: ASDisplayNode
|
||||
private let transition: NavigationTransition
|
||||
let isInteractive: Bool
|
||||
let topNode: ASDisplayNode
|
||||
let bottomNode: ASDisplayNode
|
||||
private let topNavigationBar: NavigationBar?
|
||||
@ -49,8 +50,9 @@ final class NavigationTransitionCoordinator {
|
||||
private var currentCompletion: (() -> Void)?
|
||||
private var didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)?
|
||||
|
||||
init(transition: NavigationTransition, container: ASDisplayNode, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) {
|
||||
init(transition: NavigationTransition, isInteractive: Bool, container: ASDisplayNode, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) {
|
||||
self.transition = transition
|
||||
self.isInteractive = isInteractive
|
||||
self.container = container
|
||||
self.didUpdateProgress = didUpdateProgress
|
||||
self.topNode = topNode
|
||||
@ -65,11 +67,11 @@ final class NavigationTransitionCoordinator {
|
||||
self.shadowNode.image = shadowImage
|
||||
|
||||
if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar {
|
||||
if let customTransitionNode = topNavigationBar.makeCustomTransitionNode?(bottomNavigationBar) {
|
||||
if let customTransitionNode = topNavigationBar.makeCustomTransitionNode?(bottomNavigationBar, isInteractive) {
|
||||
self.inlineNavigationBarTransition = false
|
||||
customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar)
|
||||
self.customTransitionNode = customTransitionNode
|
||||
} else if let customTransitionNode = bottomNavigationBar.makeCustomTransitionNode?(topNavigationBar) {
|
||||
} else if let customTransitionNode = bottomNavigationBar.makeCustomTransitionNode?(topNavigationBar, isInteractive) {
|
||||
self.inlineNavigationBarTransition = false
|
||||
customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar)
|
||||
self.customTransitionNode = customTransitionNode
|
||||
@ -131,9 +133,16 @@ final class NavigationTransitionCoordinator {
|
||||
let topFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(position * containerSize.width), y: 0.0), size: containerSize)
|
||||
let bottomFrame = CGRect(origin: CGPoint(x: ((position - 1.0) * containerSize.width * 0.3), y: 0.0), size: containerSize)
|
||||
|
||||
var canInvokeCompletion = false
|
||||
var hadEarlyCompletion = false
|
||||
transition.updateFrame(node: self.topNode, frame: topFrame, completion: { _ in
|
||||
completion()
|
||||
if canInvokeCompletion {
|
||||
completion()
|
||||
} else {
|
||||
hadEarlyCompletion = true
|
||||
}
|
||||
})
|
||||
canInvokeCompletion = true
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, topFrame.minX), height: self.container.bounds.size.height - dimInset)))
|
||||
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: self.dimNode.frame.maxX - shadowWidth, y: dimInset), size: CGSize(width: shadowWidth, height: containerSize.height - dimInset)))
|
||||
transition.updateAlpha(node: self.dimNode, alpha: (1.0 - position) * 0.15)
|
||||
@ -149,6 +158,10 @@ final class NavigationTransitionCoordinator {
|
||||
}
|
||||
|
||||
self.didUpdateProgress?(self.progress, transition, topFrame, bottomFrame)
|
||||
|
||||
if hadEarlyCompletion {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) {
|
||||
|
@ -68,6 +68,7 @@ public enum TapLongTapOrDoubleTapGestureRecognizerAction {
|
||||
case waitForSingleTap
|
||||
case waitForHold(timeout: Double, acceptTap: Bool)
|
||||
case fail
|
||||
case keepWithSingleTap
|
||||
}
|
||||
|
||||
public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||
@ -206,6 +207,8 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer,
|
||||
}
|
||||
|
||||
switch tapAction {
|
||||
case .keepWithSingleTap:
|
||||
break
|
||||
case .waitForSingleTap, .waitForDoubleTap:
|
||||
self.timer?.invalidate()
|
||||
let timer = Timer(timeInterval: 0.3, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.longTapEvent), userInfo: nil, repeats: false)
|
||||
@ -284,7 +287,7 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer,
|
||||
}
|
||||
|
||||
switch tapAction {
|
||||
case .waitForSingleTap:
|
||||
case .waitForSingleTap, .keepWithSingleTap:
|
||||
if let (touchLocation, _) = self.touchLocationAndTimestamp {
|
||||
self.lastRecognizedGestureAndLocation = (.tap, touchLocation)
|
||||
}
|
||||
|
@ -2020,10 +2020,10 @@ public func instantPageImageFile(account: Account, fileReference: FileMediaRefer
|
||||
}
|
||||
}
|
||||
|
||||
private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false) -> Signal<Tuple3<Data?, Data?, Bool>, NoError> {
|
||||
private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false) -> Signal<Tuple3<Data?, Data?, Bool>, NoError> {
|
||||
if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) {
|
||||
|
||||
let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource)
|
||||
let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: attemptSynchronously)
|
||||
|
||||
let signal = maybeFullSize
|
||||
|> take(1)
|
||||
@ -2037,7 +2037,7 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR
|
||||
|
||||
let thumbnail = Signal<Data?, NoError> { subscriber in
|
||||
let fetchedDisposable = fetchedThumbnail.start()
|
||||
let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in
|
||||
let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: attemptSynchronously).start(next: { next in
|
||||
subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []))
|
||||
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
|
||||
@ -2052,7 +2052,7 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR
|
||||
if autoFetchFullSize {
|
||||
fullSizeData = Signal<Tuple2<Data?, Bool>, NoError> { subscriber in
|
||||
let fetchedFullSizeDisposable = fetchedFullSize.start()
|
||||
let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in
|
||||
let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: attemptSynchronously).start(next: { next in
|
||||
subscriber.putNext(Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete))
|
||||
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
|
||||
@ -2082,8 +2082,8 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR
|
||||
}
|
||||
}
|
||||
|
||||
public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize)
|
||||
public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize, attemptSynchronously: attemptSynchronously)
|
||||
|
||||
return signal
|
||||
|> map { value in
|
||||
|
@ -1476,7 +1476,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|
||||
let inset: CGFloat = 3.0
|
||||
if let signal = peerAvatarImage(account: primary.0, peerReference: PeerReference(primary.1), authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) {
|
||||
return signal
|
||||
|> map { image -> (UIImage, UIImage)? in
|
||||
|> map { imageVersions -> (UIImage, UIImage)? in
|
||||
let image = imageVersions?.0
|
||||
if let image = image, let selectedImage = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
|
@ -135,7 +135,8 @@ public func donateSendMessageIntent(account: Account, sharedContext: SharedAccou
|
||||
signals.append(.single((peer, subject, savedMessagesAvatar)))
|
||||
} else {
|
||||
let peerAndAvatar = (peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.smallProfileImage, round: false) ?? .single(nil))
|
||||
|> map { avatarImage in
|
||||
|> map { imageVersions -> (Peer, SendMessageIntentSubject, UIImage?) in
|
||||
let avatarImage = imageVersions?.0
|
||||
return (peer, subject, avatarImage)
|
||||
}
|
||||
signals.append(peerAndAvatar)
|
||||
|
@ -70,8 +70,8 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
strongSelf.contextAction?(strongSelf.containerNode, gesture)
|
||||
}
|
||||
|
||||
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))
|
||||
/*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() {
|
||||
@ -80,8 +80,8 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
(self.view as? ChatAvatarNavigationNodeView)?.targetNode = self
|
||||
(self.view as? ChatAvatarNavigationNodeView)?.chatController = self.chatController
|
||||
|
||||
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))
|
||||
self.avatarNode.view.addGestureRecognizer(tapRecognizer)
|
||||
/*let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))
|
||||
self.avatarNode.view.addGestureRecognizer(tapRecognizer)*/
|
||||
}
|
||||
|
||||
@objc private func avatarTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
@ -106,7 +106,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func onLayout() {
|
||||
/*let bounds = self.bounds
|
||||
let bounds = self.bounds
|
||||
if self.bounds.size.height.isLessThanOrEqualTo(26.0) {
|
||||
if !self.avatarNode.bounds.size.equalTo(bounds.size) {
|
||||
self.avatarNode.font = smallFont
|
||||
@ -119,6 +119,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
}
|
||||
self.containerNode.frame = bounds.offsetBy(dx: 10.0, dy: 1.0)
|
||||
self.avatarNode.frame = bounds
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -307,6 +307,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
private var updateSlowmodeStatusTimerValue: Int32?
|
||||
|
||||
private var isDismissed = false
|
||||
|
||||
private var focusOnSearchAfterAppearance: Bool = false
|
||||
|
||||
public override var customData: Any? {
|
||||
return self.chatLocation
|
||||
@ -1869,46 +1871,72 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if case let .peer(peerId) = chatLocation, peerId != context.account.peerId, subject != .scheduledMessages {
|
||||
self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId)
|
||||
}
|
||||
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, displayAvatar: true)
|
||||
if let avatarNode = self.chatTitleView?.avatarNode {
|
||||
avatarNode.chatController = self
|
||||
avatarNode.contextAction = { [weak self] node, gesture in
|
||||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil else {
|
||||
return
|
||||
|
||||
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder)
|
||||
self.navigationItem.titleView = self.chatTitleView
|
||||
self.chatTitleView?.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.chatLocation == .peer(strongSelf.context.account.peerId) {
|
||||
strongSelf.effectiveNavigationController?.pushViewController(PeerMediaCollectionController(context: strongSelf.context, peerId: strongSelf.context.account.peerId))
|
||||
} else {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.firstIndex(where: {
|
||||
switch $0 {
|
||||
case .chatInfo:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
} else {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.append(.chatInfo)
|
||||
return updatedContexts.sorted()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
|
||||
}, synchronousLoad: true)
|
||||
galleryController.setHintWillBePresentedInPreviewingContext(true)
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
self?.navigationButtonAction(.openChatInfo(expandAvatar: false))
|
||||
}))
|
||||
]
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
avatarNode.tapped = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var expandAvatar = false
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, peer.smallProfileImage != nil {
|
||||
expandAvatar = true
|
||||
}
|
||||
strongSelf.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar))
|
||||
}
|
||||
}
|
||||
|
||||
let chatInfoButtonItem: UIBarButtonItem
|
||||
switch chatLocation {
|
||||
case .peer:
|
||||
let avatarNode = ChatAvatarNavigationNode()
|
||||
avatarNode.chatController = self
|
||||
avatarNode.contextAction = { [weak self] node, gesture in
|
||||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil else {
|
||||
return
|
||||
}
|
||||
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
|
||||
}, synchronousLoad: true)
|
||||
galleryController.setHintWillBePresentedInPreviewingContext(true)
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
self?.navigationButtonAction(.openChatInfo(expandAvatar: true))
|
||||
}))
|
||||
]
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
|
||||
}
|
||||
chatInfoButtonItem.target = self
|
||||
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
|
||||
chatInfoButtonItem.accessibilityLabel = self.presentationData.strings.Conversation_Info
|
||||
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true), buttonItem: chatInfoButtonItem)
|
||||
|
||||
self.navigationItem.titleView = self.chatTitleView
|
||||
self.chatTitleView?.pressed = { [weak self] in
|
||||
self?.navigationButtonAction(.openChatInfo(expandAvatar: false))
|
||||
}
|
||||
|
||||
let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationMoreIcon(presentationInterfaceState.theme), style: .plain, target: self, action: #selector(self.rightNavigationButtonAction))
|
||||
//buttonItem.accessibilityLabel = strings.Conversation_Search
|
||||
chatInfoNavigationButton = ChatNavigationButton(action: .toggleInfoPanel, buttonItem: buttonItem)
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||||
if let botStart = botStart, case .interactive = botStart.behavior {
|
||||
return state.updatedBotStartPayload(botStart.payload)
|
||||
@ -1994,8 +2022,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
} else {
|
||||
imageOverride = nil
|
||||
}
|
||||
strongSelf.chatTitleView?.avatarNode?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride)
|
||||
strongSelf.chatTitleView?.avatarNode?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && imageOverride == nil && peer.smallProfileImage != nil
|
||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride)
|
||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil
|
||||
}
|
||||
|
||||
if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages {
|
||||
@ -4443,6 +4471,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
self.interfaceInteraction = interfaceInteraction
|
||||
|
||||
if self.focusOnSearchAfterAppearance {
|
||||
self.focusOnSearchAfterAppearance = false
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||||
}
|
||||
|
||||
self.chatDisplayNode.interfaceInteraction = interfaceInteraction
|
||||
|
||||
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.addTarget(self)
|
||||
@ -4700,6 +4734,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})]), in: .window(.root))
|
||||
}))
|
||||
}
|
||||
|
||||
if self.focusOnSearchAfterAppearance {
|
||||
self.focusOnSearchAfterAppearance = false
|
||||
if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode {
|
||||
searchNode.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillDisappear(_ animated: Bool) {
|
||||
@ -5081,8 +5122,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.leftNavigationButton = nil
|
||||
}
|
||||
|
||||
self.chatTitleView?.displayAvatar = updatedChatPresentationInterfaceState.interfaceState.selectionState == nil
|
||||
|
||||
if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) {
|
||||
if self.rightNavigationButton != button {
|
||||
var animated = transition.isAnimated
|
||||
@ -8456,6 +8495,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch() {
|
||||
self.focusOnSearchAfterAppearance = true
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
|
@ -1628,10 +1628,11 @@ private final class MergedAvatarsNode: ASDisplayNode {
|
||||
if self.disposables[peer.peerId] == nil {
|
||||
if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: mergedImageSize, height: mergedImageSize), synchronousLoad: synchronousLoad) {
|
||||
let disposable = (signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] imageVersions in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let image = imageVersions?.0
|
||||
if let image = image {
|
||||
strongSelf.images[peer.peerId] = image
|
||||
strongSelf.setNeedsDisplay()
|
||||
|
@ -101,8 +101,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
private let button: HighlightTrackingButtonNode
|
||||
|
||||
let avatarNode: ChatAvatarNavigationNode?
|
||||
|
||||
private var validLayout: (CGSize, CGRect)?
|
||||
|
||||
private var titleLeftIcon: ChatTitleIcon = .none
|
||||
@ -179,15 +177,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
var pressed: (() -> Void)?
|
||||
|
||||
var displayAvatar: Bool = true {
|
||||
didSet {
|
||||
if self.displayAvatar != oldValue {
|
||||
self.avatarNode?.isHidden = !self.displayAvatar
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var titleContent: ChatTitleContent? {
|
||||
didSet {
|
||||
if let titleContent = self.titleContent {
|
||||
@ -480,7 +469,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, displayAvatar: Bool) {
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -511,11 +500,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
self.activityNode = ChatTitleActivityNode()
|
||||
self.button = HighlightTrackingButtonNode()
|
||||
if displayAvatar {
|
||||
self.avatarNode = ChatAvatarNavigationNode()
|
||||
} else {
|
||||
self.avatarNode = nil
|
||||
}
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
@ -526,7 +510,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.contentContainer.addSubnode(self.titleNode)
|
||||
self.contentContainer.addSubnode(self.activityNode)
|
||||
self.addSubnode(self.button)
|
||||
self.avatarNode.flatMap(self.contentContainer.addSubnode)
|
||||
|
||||
self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in
|
||||
self?.updateStatus()
|
||||
@ -541,16 +524,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
strongSelf.titleCredibilityIconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.titleNode.alpha = 0.4
|
||||
strongSelf.activityNode.alpha = 0.4
|
||||
strongSelf.titleCredibilityIconNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.titleNode.alpha = 1.0
|
||||
strongSelf.activityNode.alpha = 1.0
|
||||
strongSelf.titleCredibilityIconNode.alpha = 1.0
|
||||
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.titleLeftIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.titleRightIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.titleCredibilityIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -587,6 +566,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
|
||||
self.button.frame = clearBounds
|
||||
self.contentContainer.frame = clearBounds
|
||||
|
||||
var leftIconWidth: CGFloat = 0.0
|
||||
@ -604,7 +584,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
if let image = self.titleCredibilityIconNode.image {
|
||||
if self.titleCredibilityIconNode.supernode == nil {
|
||||
self.contentContainer.addSubnode(self.titleCredibilityIconNode)
|
||||
self.titleNode.addSubnode(self.titleCredibilityIconNode)
|
||||
}
|
||||
credibilityIconWidth = image.size.width + 3.0
|
||||
} else if self.titleCredibilityIconNode.supernode != nil {
|
||||
@ -620,47 +600,67 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.titleRightIconNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
var leftInset: CGFloat = 12.0
|
||||
if let avatarNode = self.avatarNode {
|
||||
let avatarSize = CGSize(width: 37.0, height: 37.0)
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: leftInset + 10.0, y: floor((size.height - avatarSize.height) / 2.0)), size: avatarSize)
|
||||
avatarNode.frame = avatarFrame
|
||||
if self.displayAvatar {
|
||||
leftInset += avatarSize.width + 10.0 + 8.0
|
||||
}
|
||||
}
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(x: leftInset - 20.0, y: 0.0), size: CGSize(width: clearBounds.width - leftInset, height: size.height))
|
||||
|
||||
let titleSideInset: CGFloat = 3.0
|
||||
var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0 - leftInset, height: size.height))
|
||||
titleSize.width += credibilityIconWidth
|
||||
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .left)
|
||||
let titleInfoSpacing: CGFloat = 0.0
|
||||
|
||||
var titleFrame: CGRect
|
||||
|
||||
if activitySize.height.isZero {
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.titleNode.frame = titleFrame
|
||||
if size.height > 40.0 {
|
||||
var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height))
|
||||
titleSize.width += credibilityIconWidth
|
||||
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center)
|
||||
let titleInfoSpacing: CGFloat = 0.0
|
||||
|
||||
var titleFrame: CGRect
|
||||
|
||||
if activitySize.height.isZero {
|
||||
titleFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - titleSize.width) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
if titleFrame.size.width < size.width {
|
||||
titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0)
|
||||
}
|
||||
self.titleNode.frame = titleFrame
|
||||
} else {
|
||||
let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing
|
||||
|
||||
titleFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
if titleFrame.size.width < size.width {
|
||||
titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0)
|
||||
}
|
||||
titleFrame.origin.x = max(titleFrame.origin.x, clearBounds.minX + leftIconWidth)
|
||||
self.titleNode.frame = titleFrame
|
||||
|
||||
var activityFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - activitySize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize)
|
||||
if activitySize.width < size.width {
|
||||
activityFrame.origin.x = -clearBounds.minX + floor((size.width - activityFrame.width) / 2.0)
|
||||
}
|
||||
self.activityNode.frame = activityFrame
|
||||
}
|
||||
|
||||
if let image = self.titleLeftIconNode.image {
|
||||
self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size)
|
||||
}
|
||||
if let image = self.titleCredibilityIconNode.image {
|
||||
self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width - image.size.width - 1.0, y: 2.0), size: image.size)
|
||||
}
|
||||
if let image = self.titleRightIconNode.image {
|
||||
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size)
|
||||
}
|
||||
} else {
|
||||
let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height))
|
||||
let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center)
|
||||
|
||||
titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
let titleInfoSpacing: CGFloat = 8.0
|
||||
let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((clearBounds.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.titleNode.frame = titleFrame
|
||||
self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize)
|
||||
|
||||
var activityFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize)
|
||||
self.activityNode.frame = activityFrame
|
||||
}
|
||||
|
||||
if let image = self.titleLeftIconNode.image {
|
||||
self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size)
|
||||
}
|
||||
if let image = self.titleCredibilityIconNode.image {
|
||||
self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 2.0), size: image.size)
|
||||
}
|
||||
if let image = self.titleRightIconNode.image {
|
||||
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size)
|
||||
if let image = self.titleLeftIconNode.image {
|
||||
self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size)
|
||||
}
|
||||
if let image = self.titleCredibilityIconNode.image {
|
||||
self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size)
|
||||
}
|
||||
if let image = self.titleRightIconNode.image {
|
||||
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
75
submodules/TelegramUI/TelegramUI/MultiScaleTextNode.swift
Normal file
75
submodules/TelegramUI/TelegramUI/MultiScaleTextNode.swift
Normal file
@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private final class MultiScaleTextStateNode: ASDisplayNode {
|
||||
let textNode: ImmediateTextNode
|
||||
|
||||
var currentLayout: MultiScaleTextLayout?
|
||||
|
||||
override init() {
|
||||
self.textNode = ImmediateTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiScaleTextState {
|
||||
let attributedText: NSAttributedString
|
||||
let constrainedSize: CGSize
|
||||
|
||||
init(attributedText: NSAttributedString, constrainedSize: CGSize) {
|
||||
self.attributedText = attributedText
|
||||
self.constrainedSize = constrainedSize
|
||||
}
|
||||
}
|
||||
|
||||
struct MultiScaleTextLayout {
|
||||
var size: CGSize
|
||||
}
|
||||
|
||||
final class MultiScaleTextNode: ASDisplayNode {
|
||||
private let stateNodes: [AnyHashable: MultiScaleTextStateNode]
|
||||
|
||||
init(stateKeys: [AnyHashable]) {
|
||||
self.stateNodes = Dictionary(stateKeys.map { ($0, MultiScaleTextStateNode()) }, uniquingKeysWith: { lhs, _ in lhs })
|
||||
|
||||
super.init()
|
||||
|
||||
for (_, node) in self.stateNodes {
|
||||
self.addSubnode(node)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(states: [AnyHashable: MultiScaleTextState]) -> [AnyHashable: MultiScaleTextLayout] {
|
||||
assert(Set(states.keys) == Set(self.stateNodes.keys))
|
||||
|
||||
var result: [AnyHashable: MultiScaleTextLayout] = [:]
|
||||
for (key, state) in states {
|
||||
if let node = self.stateNodes[key] {
|
||||
node.textNode.attributedText = state.attributedText
|
||||
let nodeSize = node.textNode.updateLayout(state.constrainedSize)
|
||||
let nodeLayout = MultiScaleTextLayout(size: nodeSize)
|
||||
node.currentLayout = nodeLayout
|
||||
node.textNode.frame = CGRect(origin: CGPoint(x: -nodeSize.width / 2.0, y: -nodeSize.height / 2.0), size: nodeSize)
|
||||
result[key] = nodeLayout
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func update(stateFractions: [AnyHashable: CGFloat], transition: ContainedViewLayoutTransition) {
|
||||
var fractionSum: CGFloat = 0.0
|
||||
for (_, fraction) in stateFractions {
|
||||
fractionSum += fraction
|
||||
}
|
||||
for (key, fraction) in stateFractions {
|
||||
if let node = self.stateNodes[key], let nodeLayout = node.currentLayout {
|
||||
transition.updateAlpha(node: node, alpha: fraction / fractionSum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -33,6 +33,10 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
||||
controller.scrollToEndOfHistory()
|
||||
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
|
||||
params.completion()
|
||||
} else if params.activateMessageSearch {
|
||||
controller.activateSearch()
|
||||
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
|
||||
params.completion()
|
||||
} else {
|
||||
let _ = params.navigationController.popToViewController(controller, animated: params.animated)
|
||||
params.completion()
|
||||
@ -65,6 +69,9 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
||||
controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, subject: params.subject, botStart: params.botStart)
|
||||
}
|
||||
controller.purposefulAction = params.purposefulAction
|
||||
if params.activateMessageSearch {
|
||||
controller.activateSearch()
|
||||
}
|
||||
let resolvedKeepStack: Bool
|
||||
switch params.keepStack {
|
||||
case .default:
|
||||
|
@ -0,0 +1,57 @@
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
|
||||
final class PeerInfoScreenCommentItem: PeerInfoScreenItem {
|
||||
let id: AnyHashable
|
||||
let text: String
|
||||
|
||||
init(id: AnyHashable, text: String) {
|
||||
self.id = id
|
||||
self.text = text
|
||||
}
|
||||
|
||||
func node() -> PeerInfoScreenItemNode {
|
||||
return PeerInfoScreenCommentItemNode()
|
||||
}
|
||||
}
|
||||
|
||||
private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private var item: PeerInfoScreenCommentItem?
|
||||
|
||||
override init() {
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
guard let item = item as? PeerInfoScreenCommentItem else {
|
||||
return 10.0
|
||||
}
|
||||
|
||||
self.item = item
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let verticalInset: CGFloat = 7.0
|
||||
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(14.0), textColor: presentationData.theme.list.freeTextColor)
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)
|
||||
|
||||
let height = textSize.height + verticalInset * 2.0
|
||||
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
|
||||
return height
|
||||
}
|
||||
}
|
@ -372,6 +372,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
)
|
||||
self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
|
||||
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
|
@ -9,34 +9,47 @@ import PeerPresenceStatusManager
|
||||
import TelegramStringFormatting
|
||||
import TelegramPresentationData
|
||||
|
||||
enum PeerInfoUpdatingAvatar {
|
||||
case none
|
||||
case image(TelegramMediaImageRepresentation)
|
||||
}
|
||||
|
||||
final class PeerInfoState {
|
||||
let isEditing: Bool
|
||||
let isSearching: Bool
|
||||
let selectedMessageIds: Set<MessageId>?
|
||||
let updatingAvatar: PeerInfoUpdatingAvatar?
|
||||
|
||||
init(
|
||||
isEditing: Bool,
|
||||
isSearching: Bool,
|
||||
selectedMessageIds: Set<MessageId>?
|
||||
selectedMessageIds: Set<MessageId>?,
|
||||
updatingAvatar: PeerInfoUpdatingAvatar?
|
||||
) {
|
||||
self.isEditing = isEditing
|
||||
self.isSearching = isSearching
|
||||
self.selectedMessageIds = selectedMessageIds
|
||||
self.updatingAvatar = updatingAvatar
|
||||
}
|
||||
|
||||
func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
|
||||
return PeerInfoState(
|
||||
isEditing: isEditing,
|
||||
isSearching: self.isSearching,
|
||||
selectedMessageIds: self.selectedMessageIds
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
updatingAvatar: self.updatingAvatar
|
||||
)
|
||||
}
|
||||
|
||||
func withSelectedMessageIds(_ selectedMessageIds: Set<MessageId>?) -> PeerInfoState {
|
||||
return PeerInfoState(
|
||||
isEditing: self.isEditing,
|
||||
isSearching: self.isSearching,
|
||||
selectedMessageIds: selectedMessageIds
|
||||
selectedMessageIds: selectedMessageIds,
|
||||
updatingAvatar: self.updatingAvatar
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatingAvatar(_ updatingAvatar: PeerInfoUpdatingAvatar?) -> PeerInfoState {
|
||||
return PeerInfoState(
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
updatingAvatar: updatingAvatar
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -50,6 +63,7 @@ final class PeerInfoScreenData {
|
||||
let isContact: Bool
|
||||
let availablePanes: [PeerInfoPaneKey]
|
||||
let groupsInCommon: [Peer]?
|
||||
let linkedDiscussionPeer: Peer?
|
||||
|
||||
init(
|
||||
peer: Peer?,
|
||||
@ -59,7 +73,8 @@ final class PeerInfoScreenData {
|
||||
globalNotificationSettings: GlobalNotificationSettings?,
|
||||
isContact: Bool,
|
||||
availablePanes: [PeerInfoPaneKey],
|
||||
groupsInCommon: [Peer]?
|
||||
groupsInCommon: [Peer]?,
|
||||
linkedDiscussionPeer: Peer?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.cachedData = cachedData
|
||||
@ -69,12 +84,21 @@ final class PeerInfoScreenData {
|
||||
self.isContact = isContact
|
||||
self.availablePanes = availablePanes
|
||||
self.groupsInCommon = groupsInCommon
|
||||
self.linkedDiscussionPeer = linkedDiscussionPeer
|
||||
}
|
||||
}
|
||||
|
||||
enum PeerInfoScreenInputUserKind {
|
||||
case user
|
||||
case bot
|
||||
case support
|
||||
}
|
||||
|
||||
enum PeerInfoScreenInputData: Equatable {
|
||||
case none
|
||||
case user(userId: PeerId, secretChatId: PeerId?, isBot: Bool)
|
||||
case user(userId: PeerId, secretChatId: PeerId?, kind: PeerInfoScreenInputUserKind)
|
||||
case channel
|
||||
case group(isSupergroup: Bool)
|
||||
}
|
||||
|
||||
func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey], NoError> {
|
||||
@ -82,7 +106,7 @@ func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Sig
|
||||
(.photoOrVideo, .media),
|
||||
(.file, .files),
|
||||
(.music, .music),
|
||||
(.voiceOrInstantVideo, .voice),
|
||||
//(.voiceOrInstantVideo, .voice),
|
||||
(.webPage, .links)
|
||||
]
|
||||
return combineLatest(tags.map { tagAndKey -> Signal<PeerInfoPaneKey?, NoError> in
|
||||
@ -127,9 +151,25 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
return .none
|
||||
}
|
||||
if let user = peer as? TelegramUser {
|
||||
return .user(userId: user.id, secretChatId: nil, isBot: user.botInfo != nil)
|
||||
let kind: PeerInfoScreenInputUserKind
|
||||
if user.flags.contains(.isSupport) {
|
||||
kind = .support
|
||||
} else if user.botInfo != nil {
|
||||
kind = .bot
|
||||
} else {
|
||||
kind = .user
|
||||
}
|
||||
return .user(userId: user.id, secretChatId: nil, kind: kind)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
return .group(isSupergroup: true)
|
||||
} else {
|
||||
return .channel
|
||||
}
|
||||
} else if let _ = peer as? TelegramGroup {
|
||||
return .group(isSupergroup: false)
|
||||
} else {
|
||||
preconditionFailure()
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
@ -144,23 +184,26 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
globalNotificationSettings: nil,
|
||||
isContact: false,
|
||||
availablePanes: [],
|
||||
groupsInCommon: nil
|
||||
groupsInCommon: nil,
|
||||
linkedDiscussionPeer: nil
|
||||
))
|
||||
case let .user(peerId, secretChatId, isBot):
|
||||
case let .user(peerId, secretChatId, kind):
|
||||
let groupsInCommonSignal: Signal<[Peer]?, NoError>
|
||||
if isBot {
|
||||
groupsInCommonSignal = .single([])
|
||||
} else {
|
||||
switch kind {
|
||||
case .user:
|
||||
groupsInCommonSignal = .single(nil)
|
||||
|> then(
|
||||
groupsInCommon(account: context.account, peerId: peerId)
|
||||
|> map(Optional.init)
|
||||
)
|
||||
default:
|
||||
groupsInCommonSignal = .single([])
|
||||
}
|
||||
enum StatusInputData: Equatable {
|
||||
case none
|
||||
case presence(TelegramUserPresence)
|
||||
case bot
|
||||
case support
|
||||
}
|
||||
let status = Signal<PeerInfoStatusData?, NoError> { subscriber in
|
||||
class Manager {
|
||||
@ -188,12 +231,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
if user.isDeleted {
|
||||
return .none
|
||||
}
|
||||
if user.flags.contains(.isSupport) {
|
||||
return .support
|
||||
}
|
||||
if user.botInfo != nil {
|
||||
return .bot
|
||||
}
|
||||
if user.flags.contains(.isSupport) {
|
||||
return .none
|
||||
}
|
||||
guard let presence = view.peerPresences[peerId] as? TelegramUserPresence else {
|
||||
return .none
|
||||
}
|
||||
@ -203,6 +246,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
switch inputData {
|
||||
case .bot:
|
||||
subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericBotStatus, isActivity: false))
|
||||
case .support:
|
||||
subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericSupportStatus, isActivity: false))
|
||||
default:
|
||||
var presence: TelegramUserPresence?
|
||||
if case let .presence(value) = inputData {
|
||||
@ -268,13 +313,124 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
globalNotificationSettings: globalNotificationSettings,
|
||||
isContact: peerView.peerIsContact,
|
||||
availablePanes: availablePanes,
|
||||
groupsInCommon: groupsInCommon
|
||||
groupsInCommon: groupsInCommon,
|
||||
linkedDiscussionPeer: nil
|
||||
)
|
||||
}
|
||||
case .channel:
|
||||
let status = context.account.viewTracker.peerView(peerId, updateData: false)
|
||||
|> map { peerView -> PeerInfoStatusData? in
|
||||
guard let channel = peerView.peers[peerId] as? TelegramChannel else {
|
||||
return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false)
|
||||
}
|
||||
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 {
|
||||
return PeerInfoStatusData(text: strings.Conversation_StatusSubscribers(memberCount), isActivity: false)
|
||||
} else {
|
||||
return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false)
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
|
||||
var combinedKeys: [PostboxViewKey] = []
|
||||
combinedKeys.append(globalNotificationsKey)
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId),
|
||||
context.account.postbox.combinedView(keys: combinedKeys),
|
||||
status
|
||||
)
|
||||
|> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in
|
||||
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
|
||||
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
|
||||
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
|
||||
globalNotificationSettings = settings
|
||||
}
|
||||
}
|
||||
|
||||
var discussionPeer: Peer?
|
||||
if let linkedDiscussionPeerId = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
|
||||
discussionPeer = peer
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
peer: peerView.peers[peerId],
|
||||
cachedData: peerView.cachedData,
|
||||
status: status,
|
||||
notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings,
|
||||
globalNotificationSettings: globalNotificationSettings,
|
||||
isContact: peerView.peerIsContact,
|
||||
availablePanes: availablePanes,
|
||||
groupsInCommon: [],
|
||||
linkedDiscussionPeer: discussionPeer
|
||||
)
|
||||
}
|
||||
case .group:
|
||||
let status = context.account.viewTracker.peerView(peerId, updateData: false)
|
||||
|> map { peerView -> PeerInfoStatusData? in
|
||||
guard let channel = peerView.peers[peerId] as? TelegramChannel else {
|
||||
return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false)
|
||||
}
|
||||
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 {
|
||||
return PeerInfoStatusData(text: strings.Conversation_StatusMembers(memberCount), isActivity: false)
|
||||
} else {
|
||||
return PeerInfoStatusData(text: strings.Group_Status, isActivity: false)
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
|
||||
var combinedKeys: [PostboxViewKey] = []
|
||||
combinedKeys.append(globalNotificationsKey)
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId),
|
||||
context.account.postbox.combinedView(keys: combinedKeys),
|
||||
status
|
||||
)
|
||||
|> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in
|
||||
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
|
||||
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
|
||||
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
|
||||
globalNotificationSettings = settings
|
||||
}
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
peer: peerView.peers[peerId],
|
||||
cachedData: peerView.cachedData,
|
||||
status: status,
|
||||
notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings,
|
||||
globalNotificationSettings: globalNotificationSettings,
|
||||
isContact: peerView.peerIsContact,
|
||||
availablePanes: availablePanes,
|
||||
groupsInCommon: [],
|
||||
linkedDiscussionPeer: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func canEditPeerInfo(peer: Peer?) -> Bool {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if channel.hasPermission(.changeInfo) {
|
||||
return true
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
switch group.role {
|
||||
case .admin, .creator:
|
||||
return true
|
||||
case .member:
|
||||
break
|
||||
}
|
||||
if !group.hasBannedPermission(.banChangeInfo) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?) -> [PeerInfoHeaderButtonKey] {
|
||||
var result: [PeerInfoHeaderButtonKey] = []
|
||||
if let user = peer as? TelegramUser {
|
||||
@ -291,6 +447,62 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?) -> [PeerInf
|
||||
if !user.isDeleted, user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||
result.append(.more)
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
var canEditGroupInfo = false
|
||||
var canEditMembers = false
|
||||
var canAddMembers = false
|
||||
var isPublic = false
|
||||
var isCreator = false
|
||||
|
||||
isPublic = channel.username != nil
|
||||
if !isPublic, let cachedChannelData = cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
|
||||
isPublic = true
|
||||
}
|
||||
|
||||
isCreator = channel.flags.contains(.isCreator)
|
||||
if channel.hasPermission(.changeInfo) {
|
||||
canEditGroupInfo = true
|
||||
}
|
||||
if channel.hasPermission(.banMembers) {
|
||||
canEditMembers = true
|
||||
}
|
||||
if channel.hasPermission(.inviteMembers) {
|
||||
canAddMembers = true
|
||||
}
|
||||
|
||||
result.append(.mute)
|
||||
result.append(.more)
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
var canEditGroupInfo = false
|
||||
var canEditMembers = false
|
||||
var canAddMembers = false
|
||||
var isPublic = false
|
||||
var isCreator = false
|
||||
|
||||
if case .creator = group.role {
|
||||
isCreator = true
|
||||
}
|
||||
switch group.role {
|
||||
case .admin, .creator:
|
||||
canEditGroupInfo = true
|
||||
canEditMembers = true
|
||||
canAddMembers = true
|
||||
case .member:
|
||||
break
|
||||
}
|
||||
if !group.hasBannedPermission(.banChangeInfo) {
|
||||
canEditGroupInfo = true
|
||||
}
|
||||
if !group.hasBannedPermission(.banAddMembers) {
|
||||
canAddMembers = true
|
||||
}
|
||||
|
||||
if canAddMembers {
|
||||
result.append(.addMember)
|
||||
}
|
||||
|
||||
result.append(.mute)
|
||||
result.append(.more)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -301,6 +513,8 @@ func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?) -> Bool {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else if peer is TelegramChannel || peer is TelegramGroup {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import TelegramPresentationData
|
||||
import PhotoResources
|
||||
import PeerAvatarGalleryUI
|
||||
import TelegramStringFormatting
|
||||
import ActivityIndicator
|
||||
|
||||
enum PeerInfoHeaderButtonKey: Hashable {
|
||||
case message
|
||||
@ -154,25 +155,19 @@ enum PeerInfoAvatarListItem: Equatable {
|
||||
}
|
||||
|
||||
final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
let imageNode: TransformImageNode
|
||||
|
||||
let isReady = Promise<Bool>()
|
||||
private var didSetReady: Bool = false
|
||||
|
||||
init(context: AccountContext, item: PeerInfoAvatarListItem) {
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
let representations: [ImageRepresentationWithReference]
|
||||
switch item {
|
||||
case let .topImage(topRepresentations):
|
||||
representations = topRepresentations
|
||||
case let .image(_, imageRepresentations):
|
||||
representations = imageRepresentations
|
||||
}
|
||||
self.imageNode.setSignal(chatAvatarGalleryPhoto(account: context.account, representations: representations, autoFetchFullSize: true), dispatchOnDisplayLink: false)
|
||||
|
||||
self.imageNode.imageUpdated = { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
@ -185,6 +180,17 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func setup(item: PeerInfoAvatarListItem, synchronous: Bool) {
|
||||
let representations: [ImageRepresentationWithReference]
|
||||
switch item {
|
||||
case let .topImage(topRepresentations):
|
||||
representations = topRepresentations
|
||||
case let .image(_, imageRepresentations):
|
||||
representations = imageRepresentations
|
||||
}
|
||||
self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, autoFetchFullSize: true, attemptSynchronously: synchronous), attemptSynchronously: synchronous, dispatchOnDisplayLink: false)
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let imageSize = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
@ -198,13 +204,23 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
|
||||
let controlsContainerNode: ASDisplayNode
|
||||
let controlsContainerTransformNode: ASDisplayNode
|
||||
let shadowNode: ASDisplayNode
|
||||
let controlsClippingNode: ASDisplayNode
|
||||
let controlsClippingOffsetNode: ASDisplayNode
|
||||
let shadowNode: ASImageNode
|
||||
|
||||
let contentNode: ASDisplayNode
|
||||
let leftHighlightNode: ASImageNode
|
||||
let rightHighlightNode: ASImageNode
|
||||
var highlightedSide: Bool?
|
||||
let stripContainerNode: ASDisplayNode
|
||||
let highlightContainerNode: ASDisplayNode
|
||||
private(set) var galleryEntries: [AvatarGalleryEntry] = []
|
||||
private var items: [PeerInfoAvatarListItem] = []
|
||||
private var itemNodes: [WrappedMediaResourceId: PeerInfoAvatarListItemNode] = [:]
|
||||
private var stripNodes: [ASImageNode] = []
|
||||
private let inactiveStripImage: UIImage
|
||||
private let activeStripImage: UIImage
|
||||
private var appliedStripNodeCurrentIndex: Int?
|
||||
private var currentIndex: Int = 0
|
||||
private var transitionFraction: CGFloat = 0.0
|
||||
|
||||
@ -229,14 +245,99 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
self.leftHighlightNode = ASImageNode()
|
||||
self.leftHighlightNode.displaysAsynchronously = false
|
||||
self.leftHighlightNode.displayWithoutProcessing = true
|
||||
self.leftHighlightNode.contentMode = .scaleToFill
|
||||
self.leftHighlightNode.image = generateImage(CGSize(width: 88.0, height: 1.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let topColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0)
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
||||
})
|
||||
self.leftHighlightNode.isHidden = true
|
||||
|
||||
self.rightHighlightNode = ASImageNode()
|
||||
self.rightHighlightNode.displaysAsynchronously = false
|
||||
self.rightHighlightNode.displayWithoutProcessing = true
|
||||
self.rightHighlightNode.contentMode = .scaleToFill
|
||||
self.rightHighlightNode.image = generateImage(CGSize(width: 88.0, height: 1.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let topColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0)
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: 0.0, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
||||
})
|
||||
self.rightHighlightNode.isHidden = true
|
||||
|
||||
self.stripContainerNode = ASDisplayNode()
|
||||
self.contentNode.addSubnode(self.stripContainerNode)
|
||||
self.inactiveStripImage = generateStretchableFilledCircleImage(diameter: 3.0, color: UIColor(white: 1.0, alpha: 0.2))!
|
||||
self.activeStripImage = generateStretchableFilledCircleImage(diameter: 3.0, color: .white)!
|
||||
|
||||
self.highlightContainerNode = ASDisplayNode()
|
||||
self.highlightContainerNode.addSubnode(self.leftHighlightNode)
|
||||
self.highlightContainerNode.addSubnode(self.rightHighlightNode)
|
||||
|
||||
self.controlsContainerNode = ASDisplayNode()
|
||||
self.controlsContainerNode.isUserInteractionEnabled = false
|
||||
|
||||
self.controlsContainerTransformNode = ASDisplayNode()
|
||||
self.controlsContainerTransformNode.isUserInteractionEnabled = false
|
||||
self.controlsClippingOffsetNode = ASDisplayNode()
|
||||
|
||||
self.shadowNode = ASDisplayNode()
|
||||
//self.shadowNode.backgroundColor = .green
|
||||
self.controlsClippingNode = ASDisplayNode()
|
||||
self.controlsClippingNode.isUserInteractionEnabled = false
|
||||
self.controlsClippingNode.clipsToBounds = true
|
||||
|
||||
self.shadowNode = ASImageNode()
|
||||
self.shadowNode.displaysAsynchronously = false
|
||||
self.shadowNode.displayWithoutProcessing = true
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
|
||||
do {
|
||||
let size = CGSize(width: 88.0, height: 88.0)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let topColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||
let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0)
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
if let image = image {
|
||||
self.shadowNode.image = generateImage(image.size, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.rotate(by: -CGFloat.pi / 2.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
@ -244,12 +345,52 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
|
||||
self.controlsContainerNode.addSubnode(self.highlightContainerNode)
|
||||
self.controlsContainerNode.addSubnode(self.shadowNode)
|
||||
self.controlsContainerTransformNode.addSubnode(self.controlsContainerNode)
|
||||
self.addSubnode(self.controlsContainerTransformNode)
|
||||
self.controlsContainerNode.addSubnode(self.stripContainerNode)
|
||||
self.controlsClippingNode.addSubnode(self.controlsContainerNode)
|
||||
self.controlsClippingOffsetNode.addSubnode(self.controlsClippingNode)
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
return strongSelf.currentIndex != 0
|
||||
}
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { _ in
|
||||
return .keepWithSingleTap
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
guard let strongSelf = self, let size = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
var highlightedSide: Bool?
|
||||
if let point = point {
|
||||
if point.x < size.width * 1.0 / 5.0 {
|
||||
if strongSelf.currentIndex != 0 {
|
||||
highlightedSide = false
|
||||
}
|
||||
} else if point.x > size.width * 4.0 / 5.0 {
|
||||
if strongSelf.currentIndex < strongSelf.items.count - 1 || strongSelf.items.count > 1 {
|
||||
highlightedSide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if strongSelf.highlightedSide != highlightedSide {
|
||||
strongSelf.highlightedSide = highlightedSide
|
||||
if let highlightedSide = highlightedSide {
|
||||
strongSelf.leftHighlightNode.isHidden = highlightedSide
|
||||
strongSelf.rightHighlightNode.isHidden = !highlightedSide
|
||||
} else {
|
||||
strongSelf.leftHighlightNode.isHidden = true
|
||||
strongSelf.rightHighlightNode.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -267,6 +408,32 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
if let size = self.validLayout, case .tap = gesture {
|
||||
if location.x < size.width * 1.0 / 5.0 {
|
||||
if self.currentIndex != 0 {
|
||||
self.currentIndex -= 1
|
||||
self.updateItems(size: size, transition: .immediate)
|
||||
}
|
||||
} else if location.x > size.width * 4.0 / 5.0 {
|
||||
if self.currentIndex < self.items.count - 1 {
|
||||
self.currentIndex += 1
|
||||
self.updateItems(size: size, transition: .immediate)
|
||||
} else if self.items.count > 1 {
|
||||
self.currentIndex = 0
|
||||
self.updateItems(size: size, transition: .immediate, synchronous: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .changed:
|
||||
@ -309,6 +476,10 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
|
||||
func update(size: CGSize, peer: Peer?, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
|
||||
self.leftHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width * 1.0 / 5.0), height: size.height))
|
||||
self.rightHighlightNode.frame = CGRect(origin: CGPoint(x: size.width - floor(size.width * 1.0 / 5.0), y: 0.0), size: CGSize(width: floor(size.width * 1.0 / 5.0), height: size.height))
|
||||
|
||||
if let peer = peer, !self.initializedList {
|
||||
self.initializedList = true
|
||||
self.disposable.set((fetchedAvatarGalleryEntries(account: self.context.account, peer: peer)
|
||||
@ -341,7 +512,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
self.updateItems(size: size, transition: transition)
|
||||
}
|
||||
|
||||
private func updateItems(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
private func updateItems(size: CGSize, transition: ContainedViewLayoutTransition, synchronous: Bool = false) {
|
||||
var validIds: [WrappedMediaResourceId] = []
|
||||
var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = []
|
||||
var additiveTransitionOffset: CGFloat = 0.0
|
||||
@ -354,7 +525,8 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
itemNode = current
|
||||
} else {
|
||||
wasAdded = true
|
||||
itemNode = PeerInfoAvatarListItemNode(context: self.context, item: self.items[i])
|
||||
itemNode = PeerInfoAvatarListItemNode(context: self.context)
|
||||
itemNode.setup(item: self.items[i], synchronous: synchronous && i == self.currentIndex)
|
||||
self.itemNodes[self.items[i].id] = itemNode
|
||||
self.contentNode.addSubnode(itemNode)
|
||||
}
|
||||
@ -387,6 +559,52 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
if self.stripNodes.count != self.items.count {
|
||||
if self.stripNodes.count < self.items.count {
|
||||
for _ in 0 ..< self.items.count - self.stripNodes.count {
|
||||
let stripNode = ASImageNode()
|
||||
stripNode.displaysAsynchronously = false
|
||||
stripNode.displayWithoutProcessing = true
|
||||
if stripNodes.count == self.currentIndex {
|
||||
stripNode.image = self.activeStripImage
|
||||
} else {
|
||||
stripNode.image = self.inactiveStripImage
|
||||
}
|
||||
self.stripNodes.append(stripNode)
|
||||
self.stripContainerNode.addSubnode(stripNode)
|
||||
}
|
||||
} else {
|
||||
for i in (self.items.count ..< self.stripNodes.count).reversed() {
|
||||
self.stripNodes[i].removeFromSupernode()
|
||||
self.stripNodes.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.appliedStripNodeCurrentIndex != self.currentIndex {
|
||||
if let appliedStripNodeCurrentIndex = self.appliedStripNodeCurrentIndex {
|
||||
if appliedStripNodeCurrentIndex >= 0 && appliedStripNodeCurrentIndex < self.stripNodes.count {
|
||||
self.stripNodes[appliedStripNodeCurrentIndex].image = self.inactiveStripImage
|
||||
}
|
||||
}
|
||||
self.appliedStripNodeCurrentIndex = self.currentIndex
|
||||
if self.currentIndex >= 0 && self.currentIndex < self.stripNodes.count {
|
||||
self.stripNodes[self.currentIndex].image = self.activeStripImage
|
||||
}
|
||||
}
|
||||
let stripInset: CGFloat = 5.0
|
||||
let stripSpacing: CGFloat = 4.0
|
||||
let stripWidth: CGFloat = floor((size.width - stripInset * 2.0 - stripSpacing * CGFloat(self.stripNodes.count - 1)) / CGFloat(self.stripNodes.count))
|
||||
var stripX: CGFloat = stripInset
|
||||
for i in 0 ..< self.stripNodes.count {
|
||||
if i == 0 && self.stripNodes.count == 1 {
|
||||
self.stripNodes[i].isHidden = true
|
||||
} else {
|
||||
self.stripNodes[i].isHidden = false
|
||||
}
|
||||
self.stripNodes[i].frame = CGRect(origin: CGPoint(x: stripX, y: 0.0), size: CGSize(width: stripWidth, height: 3.0))
|
||||
stripX += stripWidth + stripSpacing
|
||||
}
|
||||
|
||||
if let item = self.items.first, let itemNode = self.itemNodes[item.id] {
|
||||
if !self.didSetReady {
|
||||
self.didSetReady = true
|
||||
@ -429,7 +647,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
||||
if peer.isDeleted {
|
||||
overrideImage = .deletedIcon
|
||||
}
|
||||
self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: 100.0, height: 100.0))
|
||||
self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: 100.0, height: 100.0), storeUnrounded: true)
|
||||
self.isFirstAvatarLoading = false
|
||||
}
|
||||
}
|
||||
@ -439,6 +657,9 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
|
||||
let context: AccountContext
|
||||
let avatarNode: AvatarNode
|
||||
|
||||
private let updatingAvatarOverlay: ASImageNode
|
||||
private let activityIndicator: ActivityIndicator
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
|
||||
init(context: AccountContext) {
|
||||
@ -446,10 +667,24 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
|
||||
let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0))
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
|
||||
self.updatingAvatarOverlay = ASImageNode()
|
||||
self.updatingAvatarOverlay.displayWithoutProcessing = true
|
||||
self.updatingAvatarOverlay.displaysAsynchronously = false
|
||||
self.updatingAvatarOverlay.isHidden = true
|
||||
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(.white, 22.0, 1.0, false))
|
||||
self.activityIndicator.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0))
|
||||
self.updatingAvatarOverlay.frame = self.avatarNode.frame
|
||||
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
||||
self.activityIndicator.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(self.avatarNode.frame.midX - indicatorSize.width / 2.0), y: floorToScreenPixels(self.avatarNode.frame.midY - indicatorSize.height / 2.0)), size: indicatorSize)
|
||||
|
||||
self.addSubnode(self.updatingAvatarOverlay)
|
||||
self.addSubnode(self.activityIndicator)
|
||||
|
||||
self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
@ -460,11 +695,42 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func update(peer: Peer?, theme: PresentationTheme) {
|
||||
func update(peer: Peer?, updatingAvatar: PeerInfoUpdatingAvatar?, theme: PresentationTheme) {
|
||||
if let peer = peer {
|
||||
self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, synchronousLoad: false, displayDimensions: CGSize(width: 100.0, height: 100.0))
|
||||
let overrideImage: AvatarNodeImageOverride?
|
||||
if canEditPeerInfo(peer: peer) {
|
||||
if let updatingAvatar = updatingAvatar {
|
||||
switch updatingAvatar {
|
||||
case let .image(representation):
|
||||
overrideImage = .image(representation)
|
||||
case .none:
|
||||
overrideImage = .none
|
||||
}
|
||||
self.activityIndicator.isHidden = false
|
||||
self.updatingAvatarOverlay.isHidden = false
|
||||
if self.updatingAvatarOverlay.image == nil {
|
||||
self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: 100.0, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil)
|
||||
}
|
||||
} else {
|
||||
overrideImage = .editAvatarIcon
|
||||
self.activityIndicator.isHidden = true
|
||||
self.updatingAvatarOverlay.isHidden = true
|
||||
}
|
||||
} else {
|
||||
overrideImage = nil
|
||||
self.activityIndicator.isHidden = true
|
||||
self.updatingAvatarOverlay.isHidden = true
|
||||
}
|
||||
self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, synchronousLoad: false, displayDimensions: CGSize(width: 100.0, height: 100.0))
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.avatarNode.frame.contains(point) {
|
||||
return self.avatarNode.view
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
final class PeerInfoAvatarListNode: ASDisplayNode {
|
||||
@ -534,7 +800,10 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func animateAvatarCollapse(transition: ContainedViewLayoutTransition) {
|
||||
if let currentItemNode = self.listContainerNode.currentItemNode, let avatarCopyView = self.avatarContainerNode.avatarNode.view.snapshotContentTree(), case let .animated(duration, curve) = transition {
|
||||
if let currentItemNode = self.listContainerNode.currentItemNode, let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage, case let .animated(duration, curve) = transition {
|
||||
let avatarCopyView = UIImageView()
|
||||
avatarCopyView.image = unroundedImage
|
||||
avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame
|
||||
avatarCopyView.center = currentItemNode.imageNode.position
|
||||
currentItemNode.view.addSubview(avatarCopyView)
|
||||
let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height
|
||||
@ -854,7 +1123,7 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
|
||||
return self.itemNodes[key]?.text
|
||||
}
|
||||
|
||||
func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, peer: Peer?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, peer: Peer?, cachedData: CachedPeerData?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let avatarSize: CGFloat = 100.0
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 10.0), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize()))
|
||||
@ -869,6 +1138,16 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
|
||||
fieldKeys.append(.lastName)
|
||||
}
|
||||
}
|
||||
} else if let _ = peer as? TelegramGroup {
|
||||
fieldKeys.append(.title)
|
||||
if canEditPeerInfo(peer: peer) {
|
||||
fieldKeys.append(.description)
|
||||
}
|
||||
} else if let _ = peer as? TelegramChannel {
|
||||
fieldKeys.append(.title)
|
||||
if canEditPeerInfo(peer: peer) {
|
||||
fieldKeys.append(.description)
|
||||
}
|
||||
}
|
||||
var hasPrevious = false
|
||||
for key in fieldKeys {
|
||||
@ -883,9 +1162,15 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
|
||||
case .lastName:
|
||||
updateText = (peer as? TelegramUser)?.lastName ?? ""
|
||||
case .title:
|
||||
updateText = (peer as? TelegramUser)?.debugDisplayTitle ?? ""
|
||||
updateText = peer?.debugDisplayTitle ?? ""
|
||||
case .description:
|
||||
break
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
updateText = cachedData.about ?? ""
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
updateText = cachedData.about ?? ""
|
||||
} else {
|
||||
updateText = ""
|
||||
}
|
||||
}
|
||||
itemNode = PeerInfoHeaderSingleLineTextFieldNode()
|
||||
self.itemNodes[key] = itemNode
|
||||
@ -902,8 +1187,10 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
|
||||
isEnabled = isContact
|
||||
case .title:
|
||||
placeholder = "Title"
|
||||
isEnabled = canEditPeerInfo(peer: peer)
|
||||
case .description:
|
||||
placeholder = "Description"
|
||||
isEnabled = canEditPeerInfo(peer: peer)
|
||||
}
|
||||
let itemHeight = itemNode.update(width: width, safeInset: safeInset, hasPrevious: hasPrevious, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText)
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)))
|
||||
@ -951,6 +1238,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
|
||||
var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)?
|
||||
var requestAvatarExpansion: (([AvatarGalleryEntry], (ASDisplayNode, CGRect, () -> (UIView?, UIView?))) -> Void)?
|
||||
var requestOpenAvatarForEditing: (() -> Void)?
|
||||
|
||||
var navigationTransition: PeerInfoHeaderNavigationTransition?
|
||||
|
||||
@ -999,6 +1287,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
self.subtitleNodeContainer.addSubnode(self.subtitleNode)
|
||||
self.regularContentNode.addSubnode(self.subtitleNodeContainer)
|
||||
self.regularContentNode.addSubnode(self.avatarListNode)
|
||||
self.regularContentNode.addSubnode(self.avatarListNode.listContainerNode.controlsClippingOffsetNode)
|
||||
self.addSubnode(self.regularContentNode)
|
||||
self.addSubnode(self.editingContentNode)
|
||||
self.addSubnode(self.navigationButtonContainer)
|
||||
@ -1012,6 +1301,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
return (avatarNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
}))
|
||||
}
|
||||
self.editingContentNode.avatarNode.tapped = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestOpenAvatarForEditing?()
|
||||
}
|
||||
}
|
||||
|
||||
func updateAvatarIsHidden(_ isHidden: Bool) {
|
||||
@ -1029,7 +1324,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0
|
||||
self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0
|
||||
|
||||
let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, peer: state.isEditing ? peer : nil, isContact: isContact, presentationData: presentationData, transition: transition)
|
||||
let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, peer: state.isEditing ? peer : nil, cachedData: cachedData, isContact: isContact, presentationData: presentationData, transition: transition)
|
||||
transition.updateFrame(node: self.editingContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -contentOffset), size: CGSize(width: width, height: editingContentHeight)))
|
||||
|
||||
var transitionSourceHeight: CGFloat = 0.0
|
||||
@ -1041,7 +1336,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
|
||||
self.expandedBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor
|
||||
|
||||
if let navigationTransition = self.navigationTransition, let sourceAvatarNode = navigationTransition.sourceTitleView.avatarNode?.avatarNode {
|
||||
if let navigationTransition = self.navigationTransition, let sourceAvatarNode = (navigationTransition.sourceNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode {
|
||||
transitionSourceHeight = navigationTransition.sourceNavigationBar.bounds.height
|
||||
transitionFraction = navigationTransition.fraction
|
||||
transitionSourceAvatarFrame = sourceAvatarNode.view.convert(sourceAvatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view)
|
||||
@ -1049,6 +1344,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame
|
||||
|
||||
transition.updateAlpha(node: self.expandedBackgroundNode, alpha: transitionFraction)
|
||||
|
||||
if self.isAvatarExpanded, case .animated = transition, transitionFraction == 1.0 {
|
||||
self.avatarListNode.animateAvatarCollapse(transition: transition)
|
||||
}
|
||||
} else {
|
||||
let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / (212.0)))
|
||||
transition.updateAlpha(node: self.expandedBackgroundNode, alpha: backgroundTransitionFraction)
|
||||
@ -1139,10 +1438,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
self.avatarListNode.listContainerNode.isHidden = false
|
||||
if !transitionSourceAvatarFrame.width.isZero {
|
||||
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0)
|
||||
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0)
|
||||
} else {
|
||||
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0)
|
||||
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 0.0)
|
||||
}
|
||||
} else if self.avatarListNode.listContainerNode.cornerRadius != 50.0 {
|
||||
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 50.0)
|
||||
transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 50.0, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -1152,18 +1454,26 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.avatarListNode.update(size: CGSize(), isExpanded: self.isAvatarExpanded, peer: peer, theme: presentationData.theme, transition: transition)
|
||||
self.editingContentNode.avatarNode.update(peer: peer, theme: presentationData.theme)
|
||||
self.editingContentNode.avatarNode.update(peer: peer, updatingAvatar: state.updatingAvatar, theme: presentationData.theme)
|
||||
if additive {
|
||||
transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.avatarContainerNode, scale: avatarScale)
|
||||
} else {
|
||||
transition.updateSublayerTransformScale(node: self.avatarListNode.avatarContainerNode, scale: avatarScale)
|
||||
}
|
||||
let apparentAvatarFrame: CGRect
|
||||
let controlsClippingFrame: CGRect
|
||||
if self.isAvatarExpanded {
|
||||
let expandedAvatarCenter = CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - contentOffset / 2.0)
|
||||
apparentAvatarFrame = CGRect(origin: CGPoint(x: expandedAvatarCenter.x * (1.0 - transitionFraction) + transitionFraction * avatarCenter.x, y: expandedAvatarCenter.y * (1.0 - transitionFraction) + transitionFraction * avatarCenter.y), size: CGSize())
|
||||
if !transitionSourceAvatarFrame.width.isZero {
|
||||
let expandedFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize)
|
||||
controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * transitionSourceAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * transitionSourceAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * transitionSourceAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * transitionSourceAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height))
|
||||
} else {
|
||||
controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize)
|
||||
}
|
||||
} else {
|
||||
apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size)
|
||||
controlsClippingFrame = apparentAvatarFrame
|
||||
}
|
||||
if case let .animated(duration, curve) = transition, !transitionSourceAvatarFrame.width.isZero, false {
|
||||
let previousFrame = self.avatarListNode.frame
|
||||
@ -1205,10 +1515,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode, scale: innerScale)
|
||||
transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.contentNode, frame: CGRect(origin: CGPoint(x: innerDeltaX + expandedAvatarListSize.width / 2.0, y: innerDeltaY + expandedAvatarListSize.height / 2.0), size: CGSize()))
|
||||
|
||||
transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerTransformNode, frame: CGRect(origin: CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - innerDeltaY), size: CGSize()))
|
||||
transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode.controlsContainerNode, scale: 1.0 / innerScale)
|
||||
transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerNode.controlsContainerTransformNode, scale: 1.0 / avatarListContainerScale)
|
||||
transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.shadowNode, frame: CGRect(origin: CGPoint(x: -apparentAvatarFrame.minX, y: -apparentAvatarFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight)))
|
||||
transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsClippingOffsetNode, frame: CGRect(origin: controlsClippingFrame.center, size: CGSize()))
|
||||
transition.updateFrame(node: self.avatarListNode.listContainerNode.controlsClippingNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.width / 2.0, y: -controlsClippingFrame.height / 2.0), size: controlsClippingFrame.size))
|
||||
transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.minX, y: -controlsClippingFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height)))
|
||||
|
||||
transition.updateFrame(node: self.avatarListNode.listContainerNode.shadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight + 20.0)))
|
||||
transition.updateFrame(node: self.avatarListNode.listContainerNode.stripContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusBarHeight + 2.0), size: CGSize(width: expandedAvatarListSize.width, height: 3.0)))
|
||||
transition.updateFrame(node: self.avatarListNode.listContainerNode.highlightContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height)))
|
||||
transition.updateAlpha(node: self.avatarListNode.listContainerNode.controlsContainerNode, alpha: self.isAvatarExpanded ? (1.0 - transitionFraction) : 0.0)
|
||||
|
||||
if additive {
|
||||
transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale)
|
||||
@ -1247,16 +1561,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
let titleScale = (transitionFraction * transitionSourceTitleFrame.height + (1.0 - transitionFraction) * titleFrame.height * neutralTitleScale) / (titleFrame.height)
|
||||
let subtitleScale = max(0.01, min(10.0, (transitionFraction * transitionSourceSubtitleFrame.height + (1.0 - transitionFraction) * subtitleFrame.height * neutralSubtitleScale) / (subtitleFrame.height)))
|
||||
|
||||
let titleOrigin = CGPoint(x: transitionFraction * transitionSourceTitleFrame.minX + (1.0 - transitionFraction) * titleFrame.minX, y: transitionFraction * transitionSourceTitleFrame.minY + (1.0 - transitionFraction) * titleFrame.minY)
|
||||
let subtitleOrigin = CGPoint(x: transitionFraction * transitionSourceSubtitleFrame.minX + (1.0 - transitionFraction) * subtitleFrame.minX, y: transitionFraction * transitionSourceSubtitleFrame.minY + (1.0 - transitionFraction) * subtitleFrame.minY)
|
||||
let titleCenter = CGPoint(x: transitionFraction * transitionSourceTitleFrame.midX + (1.0 - transitionFraction) * titleFrame.midX, y: transitionFraction * transitionSourceTitleFrame.midY + (1.0 - transitionFraction) * titleFrame.midY)
|
||||
let subtitleCenter = CGPoint(x: transitionFraction * transitionSourceSubtitleFrame.midX + (1.0 - transitionFraction) * subtitleFrame.midX, y: transitionFraction * transitionSourceSubtitleFrame.midY + (1.0 - transitionFraction) * subtitleFrame.midY)
|
||||
|
||||
let rawTitleFrame = CGRect(origin: titleOrigin, size: titleFrame.size)
|
||||
let rawTitleFrame = CGRect(origin: CGPoint(x: titleCenter.x - titleFrame.size.width * titleScale / 2.0, y: titleCenter.y - titleFrame.size.height * titleScale / 2.0), size: CGSize(width: titleFrame.size.width * titleScale, height: titleFrame.size.height * titleScale))
|
||||
self.titleNodeRawContainer.frame = rawTitleFrame
|
||||
transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: rawTitleFrame.offsetBy(dx: rawTitleFrame.width * 0.5 * (titleScale - 1.0), dy: titleOffset + rawTitleFrame.height * 0.5 * (titleScale - 1.0)))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size))
|
||||
let rawSubtitleFrame = CGRect(origin: subtitleOrigin, size: subtitleFrame.size)
|
||||
transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: -titleFrame.size.width * 1.0 / titleScale / 2.0, y: -titleFrame.size.height * 1.0 / titleScale / 2.0), size: titleFrame.size))
|
||||
let rawSubtitleFrame = CGRect(origin: CGPoint(x: subtitleCenter.x - subtitleFrame.size.width / 2.0, y: subtitleCenter.y - subtitleFrame.size.height / 2.0), size: subtitleFrame.size)
|
||||
self.subtitleNodeRawContainer.frame = rawSubtitleFrame
|
||||
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: rawSubtitleFrame.offsetBy(dx: rawSubtitleFrame.width * 0.5 * (subtitleScale - 1.0), dy: titleOffset + rawSubtitleFrame.height * 0.5 * (subtitleScale - 1.0)))
|
||||
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: rawSubtitleFrame)
|
||||
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: subtitleFrame.size))
|
||||
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
|
||||
transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale)
|
||||
|
@ -128,7 +128,12 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
return strongSelf.scrollNode.view.contentOffset.x > .ulpOfOne
|
||||
}
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
if #available(iOS 11.0, *) {
|
||||
@ -249,7 +254,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
|
||||
}
|
||||
leftOffset += paneNodeSize.width + spacing
|
||||
}
|
||||
self.scrollNode.view.contentSize = CGSize(width: leftOffset + sideInset, height: size.height)
|
||||
self.scrollNode.view.contentSize = CGSize(width: leftOffset - spacing + sideInset, height: size.height)
|
||||
}
|
||||
|
||||
if let selectedFrame = selectedFrame {
|
||||
|
@ -28,6 +28,10 @@ import ContextUI
|
||||
import OpenInExternalAppUI
|
||||
import SafariServices
|
||||
import GalleryUI
|
||||
import LegacyUI
|
||||
import MapResourceToAvatarSizes
|
||||
import LegacyComponents
|
||||
import WebSearchUI
|
||||
|
||||
protocol PeerInfoScreenItem: class {
|
||||
var id: AnyHashable { get }
|
||||
@ -77,6 +81,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode {
|
||||
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
var contentWithBackgroundHeight: CGFloat = 0.0
|
||||
|
||||
for i in 0 ..< items.count {
|
||||
let item = items[i]
|
||||
@ -100,13 +105,28 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode {
|
||||
|
||||
let itemTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition
|
||||
|
||||
let itemHeight = itemNode.update(width: width, presentationData: presentationData, item: item, topItem: i == 0 ? nil : items[i - 1], bottomItem: (i == items.count - 1) ? nil : items[i + 1], transition: itemTransition)
|
||||
let bottomItem: PeerInfoScreenItem?
|
||||
if i == items.count - 1 {
|
||||
bottomItem = nil
|
||||
} else if items[i + 1] is PeerInfoScreenCommentItem {
|
||||
bottomItem = nil
|
||||
} else {
|
||||
bottomItem = items[i + 1]
|
||||
}
|
||||
|
||||
let itemHeight = itemNode.update(width: width, presentationData: presentationData, item: item, topItem: i == 0 ? nil : items[i - 1], bottomItem: bottomItem, transition: itemTransition)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))
|
||||
itemTransition.updateFrame(node: itemNode, frame: itemFrame)
|
||||
if wasAdded {
|
||||
itemNode.alpha = 0.0
|
||||
transition.updateAlpha(node: itemNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
if item is PeerInfoScreenCommentItem {
|
||||
|
||||
} else {
|
||||
contentWithBackgroundHeight += itemHeight
|
||||
}
|
||||
contentHeight += itemHeight
|
||||
}
|
||||
|
||||
@ -124,9 +144,9 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight)))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentWithBackgroundHeight)))
|
||||
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundHeight), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
|
||||
if contentHeight.isZero {
|
||||
transition.updateAlpha(node: self.topSeparatorNode, alpha: 0.0)
|
||||
@ -280,6 +300,12 @@ private enum PeerInfoBotCommand {
|
||||
case privacy
|
||||
}
|
||||
|
||||
private enum PeerInfoParticipantsSection {
|
||||
case members
|
||||
case admins
|
||||
case banned
|
||||
}
|
||||
|
||||
private final class PeerInfoInteraction {
|
||||
let openUsername: (String) -> Void
|
||||
let openPhone: (String) -> Void
|
||||
@ -293,6 +319,10 @@ private final class PeerInfoInteraction {
|
||||
let openShareBot: () -> Void
|
||||
let openAddBotToGroup: () -> Void
|
||||
let performBotCommand: (PeerInfoBotCommand) -> Void
|
||||
let editingOpenPublicLinkSetup: () -> Void
|
||||
let editingOpenDiscussionGroupSetup: () -> Void
|
||||
let editingToggleMessageSignatures: (Bool) -> Void
|
||||
let openParticipantsSection: (PeerInfoParticipantsSection) -> Void
|
||||
|
||||
init(
|
||||
openUsername: @escaping (String) -> Void,
|
||||
@ -306,7 +336,11 @@ private final class PeerInfoInteraction {
|
||||
openReport: @escaping () -> Void,
|
||||
openShareBot: @escaping () -> Void,
|
||||
openAddBotToGroup: @escaping () -> Void,
|
||||
performBotCommand: @escaping (PeerInfoBotCommand) -> Void
|
||||
performBotCommand: @escaping (PeerInfoBotCommand) -> Void,
|
||||
editingOpenPublicLinkSetup: @escaping () -> Void,
|
||||
editingOpenDiscussionGroupSetup: @escaping () -> Void,
|
||||
editingToggleMessageSignatures: @escaping (Bool) -> Void,
|
||||
openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void
|
||||
) {
|
||||
self.openUsername = openUsername
|
||||
self.openPhone = openPhone
|
||||
@ -320,6 +354,10 @@ private final class PeerInfoInteraction {
|
||||
self.openShareBot = openShareBot
|
||||
self.openAddBotToGroup = openAddBotToGroup
|
||||
self.performBotCommand = performBotCommand
|
||||
self.editingOpenPublicLinkSetup = editingOpenPublicLinkSetup
|
||||
self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup
|
||||
self.editingToggleMessageSignatures = editingToggleMessageSignatures
|
||||
self.openParticipantsSection = openParticipantsSection
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,38 +379,11 @@ private func peerInfoSectionItems(data: PeerInfoScreenData?, presentationData: P
|
||||
}
|
||||
if let cachedData = data.cachedData as? CachedUserData {
|
||||
if let about = cachedData.about {
|
||||
items.append(PeerInfoScreenLabeledValueItem(id: 0, label: "bio", text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil))
|
||||
items.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil))
|
||||
}
|
||||
}
|
||||
if !data.isContact {
|
||||
if let botInfo = user.botInfo {
|
||||
if botInfo.flags.contains(.worksWithGroups) {
|
||||
items.append(PeerInfoScreenActionItem(id: 6, text: presentationData.strings.UserInfo_InviteBotToGroup, action: {
|
||||
interaction.openAddBotToGroup()
|
||||
}))
|
||||
}
|
||||
items.append(PeerInfoScreenActionItem(id: 7, text: presentationData.strings.UserInfo_ShareBot, action: {
|
||||
interaction.openShareBot()
|
||||
}))
|
||||
|
||||
if let cachedData = data.cachedData as? CachedUserData, let botInfo = cachedData.botInfo {
|
||||
for command in botInfo.commands {
|
||||
if command.text == "settings" {
|
||||
items.append(PeerInfoScreenActionItem(id: 8, text: presentationData.strings.UserInfo_BotSettings, action: {
|
||||
interaction.performBotCommand(.settings)
|
||||
}))
|
||||
} else if command.text == "help" {
|
||||
items.append(PeerInfoScreenActionItem(id: 9, text: presentationData.strings.UserInfo_BotHelp, action: {
|
||||
interaction.performBotCommand(.help)
|
||||
}))
|
||||
} else if command.text == "privacy" {
|
||||
items.append(PeerInfoScreenActionItem(id: 10, text: presentationData.strings.UserInfo_BotPrivacy, action: {
|
||||
interaction.performBotCommand(.privacy)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if user.botInfo == nil {
|
||||
items.append(PeerInfoScreenActionItem(id: 3, text: "Add Contact", action: {
|
||||
interaction.openAddContact()
|
||||
}))
|
||||
@ -397,6 +408,54 @@ private func peerInfoSectionItems(data: PeerInfoScreenData?, presentationData: P
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if let channel = data.peer as? TelegramChannel {
|
||||
let ItemUsername = 1
|
||||
let ItemAbout = 2
|
||||
let ItemAdmins = 3
|
||||
let ItemMembers = 4
|
||||
let ItemBanned = 5
|
||||
let ItemReport = 6
|
||||
|
||||
if let username = channel.username {
|
||||
items.append(PeerInfoScreenLabeledValueItem(id: ItemUsername, label: presentationData.strings.Channel_LinkItem, text: "https://t.me/\(username)", textColor: .accent, action: {
|
||||
interaction.openUsername(username)
|
||||
}))
|
||||
}
|
||||
if let cachedData = data.cachedData as? CachedChannelData {
|
||||
if let about = cachedData.about {
|
||||
items.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil))
|
||||
}
|
||||
|
||||
if case .broadcast = channel.info {
|
||||
var canEditMembers = false
|
||||
if channel.hasPermission(.banMembers) {
|
||||
canEditMembers = true
|
||||
}
|
||||
if canEditMembers {
|
||||
if channel.adminRights != nil || channel.flags.contains(.isCreator) {
|
||||
let adminCount = cachedData.participantsSummary.adminCount ?? 0
|
||||
let memberCount = cachedData.participantsSummary.memberCount ?? 0
|
||||
let bannedCount = cachedData.participantsSummary.kickedCount ?? 0
|
||||
|
||||
items.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: "\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")", text: presentationData.strings.GroupInfo_Administrators, action: {
|
||||
interaction.openParticipantsSection(.admins)
|
||||
}))
|
||||
items.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: "\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")", text: presentationData.strings.Channel_Info_Subscribers, action: {
|
||||
interaction.openParticipantsSection(.members)
|
||||
}))
|
||||
items.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: "\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")", text: presentationData.strings.GroupInfo_Permissions_Removed, action: {
|
||||
interaction.openParticipantsSection(.banned)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let group = data.peer as? TelegramGroup {
|
||||
if let cachedData = data.cachedData as? CachedGroupData {
|
||||
if let about = cachedData.about {
|
||||
items.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
@ -407,34 +466,32 @@ private func editingInfoSectionItems(data: PeerInfoScreenData?, presentationData
|
||||
}
|
||||
var items: [PeerInfoScreenItem] = []
|
||||
|
||||
if let _ = data.peer as? TelegramUser {
|
||||
if let notificationSettings = data.notificationSettings {
|
||||
let notificationsLabel: String
|
||||
let soundLabel: String
|
||||
let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings ?? TelegramPeerNotificationSettings.defaultSettings
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
if until < Int32.max - 1 {
|
||||
notificationsLabel = stringForRemainingMuteInterval(strings: presentationData.strings, muteInterval: until)
|
||||
} else {
|
||||
notificationsLabel = presentationData.strings.UserInfo_NotificationsDisabled
|
||||
}
|
||||
if let notificationSettings = data.notificationSettings {
|
||||
let notificationsLabel: String
|
||||
let soundLabel: String
|
||||
let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings ?? TelegramPeerNotificationSettings.defaultSettings
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
if until < Int32.max - 1 {
|
||||
notificationsLabel = stringForRemainingMuteInterval(strings: presentationData.strings, muteInterval: until)
|
||||
} else {
|
||||
notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled
|
||||
notificationsLabel = presentationData.strings.UserInfo_NotificationsDisabled
|
||||
}
|
||||
|
||||
let globalNotificationSettings: GlobalNotificationSettings = data.globalNotificationSettings ?? GlobalNotificationSettings.defaultSettings
|
||||
soundLabel = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.privateChats.sound)
|
||||
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 0, label: notificationsLabel, text: "Notifications", action: {
|
||||
interaction.editingOpenNotificationSettings()
|
||||
}))
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 1, label: soundLabel, text: "Sound", action: {
|
||||
interaction.editingOpenSoundSettings()
|
||||
}))
|
||||
items.append(PeerInfoScreenSwitchItem(id: 2, text: "Show Message Text", value: notificationSettings.displayPreviews != .hide, toggled: { value in
|
||||
interaction.editingToggleShowMessageText(value)
|
||||
}))
|
||||
} else {
|
||||
notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled
|
||||
}
|
||||
|
||||
let globalNotificationSettings: GlobalNotificationSettings = data.globalNotificationSettings ?? GlobalNotificationSettings.defaultSettings
|
||||
soundLabel = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.privateChats.sound)
|
||||
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 0, label: notificationsLabel, text: "Notifications", action: {
|
||||
interaction.editingOpenNotificationSettings()
|
||||
}))
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 1, label: soundLabel, text: "Sound", action: {
|
||||
interaction.editingOpenSoundSettings()
|
||||
}))
|
||||
items.append(PeerInfoScreenSwitchItem(id: 2, text: "Show Message Text", value: notificationSettings.displayPreviews != .hide, toggled: { value in
|
||||
interaction.editingToggleShowMessageText(value)
|
||||
}))
|
||||
}
|
||||
|
||||
return items
|
||||
@ -443,10 +500,62 @@ private func editingInfoSectionItems(data: PeerInfoScreenData?, presentationData
|
||||
private func editingActionsSectionItems(data: PeerInfoScreenData?, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [PeerInfoScreenItem] {
|
||||
var items: [PeerInfoScreenItem] = []
|
||||
if let data = data {
|
||||
if data.isContact {
|
||||
items.append(PeerInfoScreenActionItem(id: 0, text: "Delete Contact", color: .destructive, action: {
|
||||
interaction.requestDeleteContact()
|
||||
}))
|
||||
if let user = data.peer as? TelegramUser {
|
||||
if data.isContact {
|
||||
items.append(PeerInfoScreenActionItem(id: 0, text: presentationData.strings.UserInfo_DeleteContact, color: .destructive, action: {
|
||||
interaction.requestDeleteContact()
|
||||
}))
|
||||
}
|
||||
} else if let channel = data.peer as? TelegramChannel {
|
||||
if channel.flags.contains(.isCreator) {
|
||||
let linkText: String
|
||||
if let username = channel.username {
|
||||
linkText = "@\(username)"
|
||||
} else {
|
||||
linkText = presentationData.strings.Channel_Setup_TypePrivate
|
||||
}
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 1, label: linkText, text: presentationData.strings.Channel_TypeSetup_Title, action: {
|
||||
interaction.editingOpenPublicLinkSetup()
|
||||
}))
|
||||
|
||||
let discussionGroupTitle: String
|
||||
if let cachedData = data.cachedData as? CachedChannelData {
|
||||
if let peer = data.linkedDiscussionPeer {
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
discussionGroupTitle = "@\(addressName)"
|
||||
} else {
|
||||
discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
}
|
||||
} else {
|
||||
discussionGroupTitle = presentationData.strings.Channel_DiscussionGroupAdd
|
||||
}
|
||||
} else {
|
||||
discussionGroupTitle = "..."
|
||||
}
|
||||
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 2, label: discussionGroupTitle, text: presentationData.strings.Channel_DiscussionGroup, action: {
|
||||
interaction.editingOpenDiscussionGroupSetup()
|
||||
}))
|
||||
//items.append(PeerInfoScreenCommentItem(id: 3, text: presentationData.strings.Channel_DiscussionGroupInfo))
|
||||
|
||||
let messagesShouldHaveSignatures: Bool
|
||||
switch channel.info {
|
||||
case let .broadcast(info):
|
||||
messagesShouldHaveSignatures = info.flags.contains(.messagesShouldHaveSignatures)
|
||||
default:
|
||||
messagesShouldHaveSignatures = false
|
||||
}
|
||||
items.append(PeerInfoScreenSwitchItem(id: 4, text: presentationData.strings.Channel_SignMessages, value: messagesShouldHaveSignatures, toggled: { value in
|
||||
interaction.editingToggleMessageSignatures(value)
|
||||
}))
|
||||
items.append(PeerInfoScreenCommentItem(id: 5, text: presentationData.strings.Channel_SignMessages_Help))
|
||||
}
|
||||
} else if let group = data.peer as? TelegramGroup {
|
||||
if case .creator = group.role {
|
||||
items.append(PeerInfoScreenDisclosureItem(id: 1, label: presentationData.strings.Group_Setup_TypePrivate, text: presentationData.strings.Channel_TypeSetup_Title, action: {
|
||||
interaction.editingOpenPublicLinkSetup()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
@ -486,13 +595,17 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
private(set) var data: PeerInfoScreenData?
|
||||
private(set) var state = PeerInfoState(
|
||||
isEditing: false,
|
||||
isSearching: false,
|
||||
selectedMessageIds: nil
|
||||
selectedMessageIds: nil,
|
||||
updatingAvatar: nil
|
||||
)
|
||||
private var dataDisposable: Disposable?
|
||||
|
||||
private let activeActionDisposable = MetaDisposable()
|
||||
private let resolveUrlDisposable = MetaDisposable()
|
||||
private let toggleShouldChannelMessagesSignaturesDisposable = MetaDisposable()
|
||||
|
||||
private let updateAvatarDisposable = MetaDisposable()
|
||||
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
var ready: Promise<Bool> {
|
||||
@ -507,6 +620,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
|
||||
self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded)
|
||||
self.infoSection = PeerInfoScreenItemSectionContainerNode(id: 0)
|
||||
@ -552,6 +666,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
},
|
||||
performBotCommand: { [weak self] command in
|
||||
self?.performBotCommand(command: command)
|
||||
},
|
||||
editingOpenPublicLinkSetup: { [weak self] in
|
||||
self?.editingOpenPublicLinkSetup()
|
||||
},
|
||||
editingOpenDiscussionGroupSetup: { [weak self] in
|
||||
self?.editingOpenDiscussionGroupSetup()
|
||||
},
|
||||
editingToggleMessageSignatures: { [weak self] value in
|
||||
self?.editingToggleMessageSignatures(value: value)
|
||||
},
|
||||
openParticipantsSection: { [weak self] section in
|
||||
self?.openParticipantsSection(section: section)
|
||||
}
|
||||
)
|
||||
|
||||
@ -950,6 +1076,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}))
|
||||
}
|
||||
|
||||
self.headerNode.requestOpenAvatarForEditing = { [weak self] in
|
||||
self?.openAvatarForEditing()
|
||||
}
|
||||
|
||||
self.headerNode.navigationButtonContainer.performAction = { [weak self] key in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -970,8 +1100,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
strongSelf.controller?.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, style: .plain, target: strongSelf, action: #selector(strongSelf.editingCancelPressed)), animated: true)
|
||||
case .done, .cancel:
|
||||
if case .done = key {
|
||||
if let data = strongSelf.data, data.isContact {
|
||||
if let peer = data.peer as? TelegramUser {
|
||||
guard let data = strongSelf.data else {
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||
return
|
||||
}
|
||||
if let peer = data.peer as? TelegramUser {
|
||||
if data.isContact {
|
||||
let firstName = strongSelf.headerNode.editingContentNode.editingTextForKey(.firstName) ?? ""
|
||||
let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? ""
|
||||
|
||||
@ -1012,7 +1146,44 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
} else {
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||
}
|
||||
} else {
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||
}
|
||||
} else if let group = data.peer as? TelegramGroup, canEditPeerInfo(peer: group) {
|
||||
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
|
||||
|
||||
} else if let channel = data.peer as? TelegramChannel, canEditPeerInfo(peer: channel) {
|
||||
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
|
||||
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
|
||||
|
||||
var updateDataSignals: [Signal<Never, Void>] = []
|
||||
|
||||
if title != channel.title {
|
||||
updateDataSignals.append(
|
||||
updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title)
|
||||
|> ignoreValues
|
||||
|> mapError { _ in return Void() }
|
||||
)
|
||||
}
|
||||
if description != (data.cachedData as? CachedChannelData)?.about {
|
||||
updateDataSignals.append(
|
||||
updatePeerDescription(account: strongSelf.context.account, peerId: channel.id, description: description.isEmpty ? nil : description)
|
||||
|> ignoreValues
|
||||
|> mapError { _ in return Void() }
|
||||
)
|
||||
}
|
||||
strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals)
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||
}, completed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||
}))
|
||||
} else {
|
||||
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel)
|
||||
}
|
||||
@ -1060,6 +1231,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self.activeActionDisposable.dispose()
|
||||
self.resolveUrlDisposable.dispose()
|
||||
self.hiddenAvatarRepresentationDisposable.dispose()
|
||||
self.toggleShouldChannelMessagesSignaturesDisposable.dispose()
|
||||
self.updateAvatarDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -1242,16 +1415,86 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
let dismissAction: () -> Void = { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
}
|
||||
var reportSpam = false
|
||||
var deleteChat = false
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.ChatSearch_SearchPlaceholder, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openChatWithMessageSearch()
|
||||
}))
|
||||
if let user = peer as? TelegramUser {
|
||||
if let botInfo = user.botInfo {
|
||||
if botInfo.flags.contains(.worksWithGroups) {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_InviteBotToGroup, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openAddBotToGroup()
|
||||
}))
|
||||
}
|
||||
if user.username != nil {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_ShareBot, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openShareBot()
|
||||
}))
|
||||
}
|
||||
|
||||
if let cachedData = data.cachedData as? CachedUserData, let botInfo = cachedData.botInfo {
|
||||
for command in botInfo.commands {
|
||||
if command.text == "settings" {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotSettings, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.performBotCommand(command: .settings)
|
||||
}))
|
||||
} else if command.text == "help" {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotHelp, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.performBotCommand(command: .help)
|
||||
}))
|
||||
} else if command.text == "privacy" {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotPrivacy, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.performBotCommand(command: .privacy)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openStartSecretChat()
|
||||
}))
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
var canReport = true
|
||||
if channel.isVerified {
|
||||
canReport = false
|
||||
}
|
||||
if channel.adminRights != nil {
|
||||
canReport = false
|
||||
}
|
||||
if channel.flags.contains(.isCreator) {
|
||||
canReport = false
|
||||
}
|
||||
if canReport {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.ReportPeer_Report, color: .destructive, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openReport()
|
||||
}))
|
||||
}
|
||||
if channel.flags.contains(.isCreator) {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openDeleteChannel()
|
||||
}))
|
||||
} else {
|
||||
if case .member = channel.participationStatus {
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: { [weak self] in
|
||||
dismissAction()
|
||||
self?.openLeaveChannel()
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
|
||||
}
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
@ -1263,6 +1506,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}
|
||||
}
|
||||
|
||||
private func openChatWithMessageSearch() {
|
||||
if let navigationController = (self.controller?.navigationController as? NavigationController) {
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), activateMessageSearch: true))
|
||||
}
|
||||
}
|
||||
|
||||
private func openStartSecretChat() {
|
||||
let peerId = self.peerId
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, PeerId?) in
|
||||
@ -1710,6 +1959,235 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
})
|
||||
}
|
||||
|
||||
private func editingOpenPublicLinkSetup() {
|
||||
self.controller?.push(channelVisibilityController(context: self.context, peerId: self.peerId, mode: .generic, upgradedToSupergroup: { _, f in f() }))
|
||||
}
|
||||
|
||||
private func editingOpenDiscussionGroupSetup() {
|
||||
self.controller?.push(channelDiscussionGroupSetupController(context: self.context, peerId: self.peerId))
|
||||
}
|
||||
|
||||
private func editingToggleMessageSignatures(value: Bool) {
|
||||
self.toggleShouldChannelMessagesSignaturesDisposable.set(toggleShouldChannelMessagesSignatures(account: self.context.account, peerId: self.peerId, enabled: value).start())
|
||||
}
|
||||
|
||||
private func openParticipantsSection(section: PeerInfoParticipantsSection) {
|
||||
switch section {
|
||||
case .members:
|
||||
self.controller?.push(channelMembersController(context: self.context, peerId: self.peerId))
|
||||
case .admins:
|
||||
self.controller?.push(channelAdminsController(context: self.context, peerId: self.peerId))
|
||||
case .banned:
|
||||
self.controller?.push(channelBlacklistController(context: self.context, peerId: self.peerId))
|
||||
}
|
||||
}
|
||||
|
||||
private func openDeleteChannel() {
|
||||
let peerId = self.peerId
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
let presentationData = strongSelf.presentationData
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
}
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: presentationData.strings.ChannelInfo_DeleteChannelConfirmation),
|
||||
ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: {
|
||||
dismissAction()
|
||||
self?.deletePeerChat(peer: peer, globally: true)
|
||||
}),
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
private func openLeaveChannel() {
|
||||
let peerId = self.peerId
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let strongSelf = self, let peer = peer else {
|
||||
return
|
||||
}
|
||||
let presentationData = strongSelf.presentationData
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
}
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: {
|
||||
dismissAction()
|
||||
self?.deletePeerChat(peer: peer, globally: false)
|
||||
}),
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
private func deletePeerChat(peer: Peer, globally: Bool) {
|
||||
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
guard let tabController = navigationController.viewControllers.first as? TabBarController else {
|
||||
return
|
||||
}
|
||||
for childController in tabController.controllers {
|
||||
if let chatListController = childController as? ChatListController {
|
||||
chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in
|
||||
if deleted {
|
||||
navigationController?.popToRoot(animated: true)
|
||||
}
|
||||
}, removed: {
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openAvatarForEditing() {
|
||||
guard let peer = self.data?.peer, canEditPeerInfo(peer: peer) else {
|
||||
return
|
||||
}
|
||||
|
||||
let peerId = self.peerId
|
||||
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.controller?.present(legacyController, in: .window(.root))
|
||||
|
||||
var hasPhotos = false
|
||||
if !peer.profileImageRepresentations.isEmpty {
|
||||
hasPhotos = true
|
||||
}
|
||||
|
||||
let completedImpl: (UIImage) -> Void = { image in
|
||||
guard let strongSelf = self, let data = image.jpegData(compressionQuality: 0.6) else {
|
||||
return
|
||||
}
|
||||
|
||||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource)
|
||||
|
||||
strongSelf.state = strongSelf.state.withUpdatingAvatar(.image(representation))
|
||||
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: uploadedPeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, resource: resource), 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 mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
|
||||
let _ = strongSelf.currentAvatarMixin.swap(mixin)
|
||||
mixin.requestSearchController = { assetsController in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in
|
||||
assetsController?.dismiss()
|
||||
completedImpl(result)
|
||||
}))
|
||||
strongSelf.controller?.present(controller, in: .window(.root))
|
||||
}
|
||||
mixin.didFinishWithImage = { image in
|
||||
if let image = image {
|
||||
completedImpl(image)
|
||||
}
|
||||
}
|
||||
mixin.didFinishWithDelete = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = strongSelf.currentAvatarMixin.swap(nil)
|
||||
if let profileImage = peer.smallProfileImage {
|
||||
strongSelf.state = strongSelf.state.withUpdatingAvatar(.none)
|
||||
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
|
||||
}
|
||||
}))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func deleteMessages(messageIds: Set<MessageId>?) {
|
||||
if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty {
|
||||
self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds)
|
||||
@ -2353,7 +2831,7 @@ public final class PeerInfoScreen: ViewController {
|
||||
badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor,
|
||||
badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor
|
||||
), strings: baseNavigationBarPresentationData.strings))
|
||||
self.navigationBar?.makeCustomTransitionNode = { [weak self] other in
|
||||
self.navigationBar?.makeCustomTransitionNode = { [weak self] other, isInteractive in
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
@ -2363,7 +2841,10 @@ public final class PeerInfoScreen: ViewController {
|
||||
if strongSelf.controllerNode.scrollNode.view.contentOffset.y > .ulpOfOne {
|
||||
return nil
|
||||
}
|
||||
if strongSelf.controllerNode.headerNode.isAvatarExpanded {
|
||||
if isInteractive && strongSelf.controllerNode.headerNode.isAvatarExpanded {
|
||||
return nil
|
||||
}
|
||||
if other.contentNode != nil {
|
||||
return nil
|
||||
}
|
||||
if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId {
|
||||
@ -2456,7 +2937,6 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
||||
private var previousBackButtonArrow: ASDisplayNode?
|
||||
private var currentBackButtonArrow: ASDisplayNode?
|
||||
private var previousBackButtonBadge: ASDisplayNode?
|
||||
private var previousRightButton: ASDisplayNode?
|
||||
private var currentBackButton: ASDisplayNode?
|
||||
|
||||
private var previousTitleNode: (ASDisplayNode, TextNode)?
|
||||
@ -2500,10 +2980,6 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
||||
self.previousBackButtonBadge = previousBackButtonBadge
|
||||
self.addSubnode(previousBackButtonBadge)
|
||||
}
|
||||
if let previousRightButton = bottomNavigationBar.makeTransitionRightButtonNode(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) {
|
||||
self.previousRightButton = previousRightButton
|
||||
self.addSubnode(previousRightButton)
|
||||
}
|
||||
if let currentBackButton = topNavigationBar.makeTransitionBackButtonNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) {
|
||||
self.currentBackButton = currentBackButton
|
||||
self.addSubnode(currentBackButton)
|
||||
@ -2512,12 +2988,14 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
||||
let previousTitleNode = previousTitleView.titleNode.makeCopy()
|
||||
let previousTitleContainerNode = ASDisplayNode()
|
||||
previousTitleContainerNode.addSubnode(previousTitleNode)
|
||||
previousTitleNode.frame = previousTitleNode.frame.offsetBy(dx: -previousTitleNode.frame.width / 2.0, dy: -previousTitleNode.frame.height / 2.0)
|
||||
self.previousTitleNode = (previousTitleContainerNode, previousTitleNode)
|
||||
self.addSubnode(previousTitleContainerNode)
|
||||
|
||||
let previousStatusNode = previousTitleView.activityNode.makeCopy()
|
||||
let previousStatusContainerNode = ASDisplayNode()
|
||||
previousStatusContainerNode.addSubnode(previousStatusNode)
|
||||
previousStatusNode.frame = previousStatusNode.frame.offsetBy(dx: -previousStatusNode.frame.width / 2.0, dy: -previousStatusNode.frame.height / 2.0)
|
||||
self.previousStatusNode = (previousStatusContainerNode, previousStatusNode)
|
||||
self.addSubnode(previousStatusContainerNode)
|
||||
}
|
||||
@ -2553,12 +3031,6 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
||||
transition.updateAlpha(node: previousBackButtonBadge, alpha: fraction)
|
||||
}
|
||||
|
||||
if let previousRightButton = self.previousRightButton {
|
||||
let previousRightButtonFrame = bottomNavigationBar.rightButtonNode.view.convert(bottomNavigationBar.rightButtonNode.view.bounds, to: bottomNavigationBar.view)
|
||||
previousRightButton.frame = previousRightButtonFrame
|
||||
transition.updateAlpha(node: previousRightButton, alpha: fraction)
|
||||
}
|
||||
|
||||
if let currentBackButton = self.currentBackButton {
|
||||
let currentBackButtonFrame = topNavigationBar.backButtonNode.view.convert(topNavigationBar.backButtonNode.view.bounds, to: topNavigationBar.view)
|
||||
transition.updateFrame(node: currentBackButton, frame: currentBackButtonFrame.offsetBy(dx: fraction * 12.0, dy: 0.0))
|
||||
@ -2566,7 +3038,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
||||
transition.updateAlpha(node: currentBackButton, alpha: (1.0 - fraction))
|
||||
}
|
||||
|
||||
if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let avatarNode = previousTitleView.avatarNode, let (previousTitleContainerNode, previousTitleNode) = self.previousTitleNode, let (previousStatusContainerNode, previousStatusNode) = self.previousStatusNode {
|
||||
if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let _ = (bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode, let (previousTitleContainerNode, previousTitleNode) = self.previousTitleNode, let (previousStatusContainerNode, previousStatusNode) = self.previousStatusNode {
|
||||
let previousTitleFrame = previousTitleView.titleNode.view.convert(previousTitleView.titleNode.bounds, to: bottomNavigationBar.view)
|
||||
let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view)
|
||||
|
||||
@ -2578,10 +3050,10 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig
|
||||
let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNode.bounds.height) / previousTitleNode.bounds.height
|
||||
let subtitleScale = max(0.01, min(10.0, (fraction * previousStatusNode.bounds.height + (1.0 - fraction) * self.headerNode.subtitleNode.bounds.height) / previousStatusNode.bounds.height))
|
||||
|
||||
transition.updateFrame(node: previousTitleContainerNode, frame: CGRect(origin: self.headerNode.titleNodeRawContainer.frame.origin.offsetBy(dx: previousTitleFrame.size.width * 0.5 * (titleScale - 1.0), dy: previousTitleFrame.size.height * 0.5 * (titleScale - 1.0)), size: previousTitleFrame.size))
|
||||
transition.updateFrame(node: previousTitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: previousTitleFrame.size))
|
||||
transition.updateFrame(node: previousStatusContainerNode, frame: CGRect(origin: self.headerNode.subtitleNodeRawContainer.frame.origin.offsetBy(dx: previousStatusFrame.size.width * 0.5 * (subtitleScale - 1.0), dy: previousStatusFrame.size.height * 0.5 * (subtitleScale - 1.0)), size: previousStatusFrame.size))
|
||||
transition.updateFrame(node: previousStatusNode, frame: CGRect(origin: CGPoint(), size: previousStatusFrame.size))
|
||||
transition.updateFrame(node: previousTitleContainerNode, frame: CGRect(origin: self.headerNode.titleNodeRawContainer.frame.center, size: CGSize()))
|
||||
transition.updateFrame(node: previousTitleNode, frame: CGRect(origin: CGPoint(x: -previousTitleFrame.width / 2.0, y: -previousTitleFrame.height / 2.0), size: previousTitleFrame.size))
|
||||
transition.updateFrame(node: previousStatusContainerNode, frame: CGRect(origin: self.headerNode.subtitleNodeRawContainer.frame.center, size: CGSize()))
|
||||
transition.updateFrame(node: previousStatusNode, frame: CGRect(origin: CGPoint(x: -previousStatusFrame.size.width / 2.0, y: -previousStatusFrame.size.height / 2.0), size: previousStatusFrame.size))
|
||||
|
||||
transition.updateSublayerTransformScale(node: previousTitleContainerNode, scale: titleScale)
|
||||
transition.updateSublayerTransformScale(node: previousStatusContainerNode, scale: subtitleScale)
|
||||
|
@ -1246,9 +1246,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
private let defaultChatControllerInteraction = ChatControllerInteraction.default
|
||||
|
||||
private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? {
|
||||
if let _ = peer as? TelegramGroup {
|
||||
if let _ = peer as? TelegramGroup {
|
||||
return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded)
|
||||
|
||||
return groupInfoController(context: context, peerId: peer.id)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded)
|
||||
|
||||
if case .group = channel.info {
|
||||
return groupInfoController(context: context, peerId: peer.id)
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user