Update settings screen

This commit is contained in:
Ilya Laktyushin 2020-07-07 02:48:35 +03:00
parent 76a340cff8
commit 37895d675b
61 changed files with 6484 additions and 4636 deletions

View File

@ -4134,7 +4134,7 @@ Unused sets are archived when you add more.";
"Chat.DeleteMessagesConfirmation_1" = "Delete message";
"Chat.DeleteMessagesConfirmation_any" = "Delete %@ messages";
"Settings.Search" = "Search";
"Settings.Search" = "Search Settings";
"SettingsSearch.FAQ" = "FAQ";
@ -5633,8 +5633,8 @@ Any member of this group will be able to see messages in the channel.";
"Call.RemoteVideoPaused" = "%@'s video paused";
"Settings.SetProfilePhotoOrVideo" = "Set Profile Photo or Video";
"Settings.SetNewProfilePhotoOrVideo" = "Set New Profile Photo or Video";
"Settings.SetProfilePhotoOrVideo" = "Set Photo or Video";
"Settings.SetNewProfilePhotoOrVideo" = "Set New Photo or Video";
"Settings.ViewVideo" = "View Video";
"Settings.RemoveVideo" = "Remove Video";
@ -5669,3 +5669,15 @@ Any member of this group will be able to see messages in the channel.";
"Stats.GroupShowMoreTopInviters_3_10" = "Show %@ More";
"Stats.GroupShowMoreTopInviters_many" = "Show %@ More";
"Stats.GroupShowMoreTopInviters_any" = "Show %@ More";
"Settings.AddAnotherAccount" = "Add Another Account";
"Settings.AddAnotherAccount.Help" = "You can add up to three accounts with different phone numbers.";
"ProfilePhoto.OpenGallery" = "Open Gallery";
"ProfilePhoto.SearchWeb" = "Search Web";
"ProfilePhoto.OpenInEditor" = "Open in Editor";
"Settings.EditAccount" = "Edit Account";
"Settings.EditPhoto" = "Edit Photo";
"Settings.FrequentlyAskedQuestions" = "Frequently Asked Questions";

View File

@ -21,6 +21,7 @@ public enum ChatListSearchItemHeaderType: Int32 {
case nearbyVenues
case chats
case chatTypes
case faq
}
public final class ChatListSearchItemHeader: ListViewItemHeader {
@ -107,6 +108,8 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
self.sectionHeaderNode.title = strings.Cache_ByPeerHeader.uppercased()
case .chatTypes:
self.sectionHeaderNode.title = strings.ChatList_ChatTypesSection.uppercased()
case .faq:
self.sectionHeaderNode.title = strings.Settings_FrequentlyAskedQuestions.uppercased()
}
self.sectionHeaderNode.action = actionTitle
@ -157,6 +160,8 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
self.sectionHeaderNode.title = strings.Cache_ByPeerHeader.uppercased()
case .chatTypes:
self.sectionHeaderNode.title = strings.ChatList_ChatTypesSection.uppercased()
case .faq:
self.sectionHeaderNode.title = strings.Settings_FrequentlyAskedQuestions.uppercased()
}
self.sectionHeaderNode.action = actionTitle

View File

@ -274,6 +274,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
private final var itemNodes: [ListViewItemNode] = []
private final var itemHeaderNodes: [Int64: ListViewItemHeaderNode] = [:]
public final var itemHeaderNodesAlpha: CGFloat = 1.0
public final var displayedItemRangeChanged: (ListViewDisplayedItemRange, Any?) -> Void = { _, _ in }
public private(set) final var displayedItemRange: ListViewDisplayedItemRange = ListViewDisplayedItemRange(loadedRange: nil, visibleRange: nil)
@ -3215,6 +3217,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
}
visibleHeaderNodes.insert(id)
let initialHeaderNodeAlpha = self.itemHeaderNodesAlpha
if let headerNode = self.itemHeaderNodes[id] {
switch transition.0 {
case .immediate:
@ -3252,15 +3256,16 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
headerNode.animateRemoved(duration: 0.2)
}
} else if hasValidNodes && headerNode.alpha.isZero {
headerNode.alpha = 1.0
headerNode.alpha = initialHeaderNodeAlpha
if animateInsertion {
headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
headerNode.layer.animateAlpha(from: 0.0, to: initialHeaderNodeAlpha, duration: 0.2)
headerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2)
}
}
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: transition.0)
} else {
let headerNode = item.node()
headerNode.alpha = initialHeaderNodeAlpha
if headerNode.item !== item {
item.updateNode(headerNode, previous: nil, next: nil)
headerNode.item = item
@ -3276,7 +3281,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.addSubnode(headerNode)
}
if animateInsertion {
headerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
headerNode.layer.animateAlpha(from: 0.0, to: initialHeaderNodeAlpha, duration: 0.3)
headerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.3)
}
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate)

View File

@ -107,7 +107,7 @@ open class NavigationBar: ASDisplayNode {
return 38.0
}
private var presentationData: NavigationBarPresentationData
var presentationData: NavigationBarPresentationData
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool)?
private var requestedLayout: Bool = false

View File

@ -311,6 +311,9 @@ open class TabBarController: ViewController {
self.navigationBar?.setSecondaryContentNode(currentController.navigationBar?.secondaryContentNode)
currentController.displayNode.recursivelyEnsureDisplaySynchronously(true)
self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle
if let navigationBarPresentationData = currentController.navigationBar?.presentationData {
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
}
} else {
self.navigationItem.title = nil
self.navigationItem.leftBarButtonItem = nil

View File

@ -487,6 +487,32 @@ public enum TabBarItemContextActionType {
}
}
public func setNavigationBarPresentationData(_ presentationData: NavigationBarPresentationData, animated: Bool) {
if animated, let navigationBar = self.navigationBar {
UIView.transition(with: navigationBar.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
}, completion: nil)
}
self.navigationBar?.updatePresentationData(presentationData)
if let parent = self.parent as? TabBarController {
if parent.currentController === self {
if animated, let navigationBar = parent.navigationBar {
UIView.transition(with: navigationBar.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
}, completion: nil)
}
parent.navigationBar?.updatePresentationData(presentationData)
}
}
}
public func setStatusBarStyle(_ style: StatusBarStyle, animated: Bool) {
self.statusBar.updateStatusBarStyle(style, animated: animated)
if let parent = self.parent as? TabBarController {
if parent.currentController === self {
parent.statusBar.updateStatusBarStyle(style, animated: animated)
}
}
}
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.view.window?.rootViewController?.present(viewControllerToPresent, animated: flag, completion: completion)
}

View File

@ -49,9 +49,10 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
let maxLength: ItemListMultilineInputItemTextLimit?
let minimalHeight: CGFloat?
let inlineAction: ItemListMultilineInputInlineAction?
let noInsets: Bool
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) {
public init(presentationData: ItemListPresentationData, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil, noInsets: Bool = false) {
self.presentationData = presentationData
self.text = text
self.placeholder = placeholder
@ -69,6 +70,7 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
self.tag = tag
self.action = action
self.inlineAction = inlineAction
self.noInsets = noInsets
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -241,7 +243,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod
}
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = itemListNeighborsGroupedInsets(neighbors)
let insets = item.noInsets ? UIEdgeInsets() : itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
@ -325,8 +327,10 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if !item.noInsets {
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText

View File

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

View File

@ -49,7 +49,7 @@
[_wrapperView addSubview:_previewView];
[camera attachPreviewView:_previewView];
_iconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"AttachmentMenuInteractiveCameraIcon")];
_iconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Editor/Camera"]];
[self addSubview:_iconView];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)]];

View File

@ -13,16 +13,13 @@ NSString *const TGAttachmentGifCellIdentifier = @"AttachmentGifCell";
{
_typeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_typeLabel.backgroundColor = [UIColor clearColor];
_typeLabel.font = TGSystemFontOfSize(12);
_typeLabel.font = TGBoldSystemFontOfSize(13);
_typeLabel.textColor = [UIColor whiteColor];
_typeLabel.text = @"GIF";
[self addSubview:_typeLabel];
[_typeLabel sizeToFit];
CGSize typeSize = CGSizeMake(ceil(_typeLabel.frame.size.width), ceil(_typeLabel.frame.size.height));
_typeLabel.frame = CGRectMake(4, self.frame.size.height - typeSize.height - 2, typeSize.width, typeSize.height);
_gradientView.hidden = false;
[self bringSubviewToFront:_cornersView];
@ -33,7 +30,9 @@ NSString *const TGAttachmentGifCellIdentifier = @"AttachmentGifCell";
- (void)layoutSubviews
{
[super layoutSubviews];
_typeLabel.frame = CGRectMake(4, self.frame.size.height - _typeLabel.frame.size.height - 2, _typeLabel.frame.size.width, _typeLabel.frame.size.height);
CGSize typeSize = _typeLabel.frame.size;
_typeLabel.frame = CGRectMake(self.frame.size.width - typeSize.width - 3.0, self.frame.size.height - typeSize.height - 1.0, typeSize.width, typeSize.height);
}
@end

View File

@ -27,13 +27,9 @@ NSString *const TGAttachmentVideoCellIdentifier = @"AttachmentVideoCell";
self = [super initWithFrame:frame];
if (self != nil)
{
_iconView = [[UIImageView alloc] init];
_iconView.contentMode = UIViewContentModeCenter;
[self addSubview:_iconView];
_durationLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_durationLabel.backgroundColor = [UIColor clearColor];
_durationLabel.font = TGSystemFontOfSize(12);
_durationLabel.font = TGBoldSystemFontOfSize(13);
_durationLabel.textColor = [UIColor whiteColor];
[self addSubview:_durationLabel];
@ -44,10 +40,7 @@ NSString *const TGAttachmentVideoCellIdentifier = @"AttachmentVideoCell";
_adjustmentsDisposable = [[SMetaDisposable alloc] init];
if (iosMajorVersion() >= 11)
{
_iconView.accessibilityIgnoresInvertColors = true;
_durationLabel.accessibilityIgnoresInvertColors = true;
}
}
return self;
}
@ -67,13 +60,6 @@ NSString *const TGAttachmentVideoCellIdentifier = @"AttachmentVideoCell";
durationFrame.size = CGSizeMake(ceil(_durationLabel.frame.size.width), ceil(_durationLabel.frame.size.height));
_durationLabel.frame = durationFrame;
if (asset.subtypes & TGMediaAssetSubtypeVideoTimelapse)
_iconView.image = TGComponentsImageNamed(@"ModernMediaItemTimelapseIcon");
else if (asset.subtypes & TGMediaAssetSubtypeVideoHighFrameRate)
_iconView.image = TGComponentsImageNamed(@"ModernMediaItemSloMoIcon");
else
_iconView.image = TGComponentsImageNamed(@"ModernMediaItemVideoIcon");
SSignal *adjustmentsSignal = [self.editingContext adjustmentsSignalForItem:self.asset];
__weak TGAttachmentVideoCell *weakSelf = self;
@ -155,11 +141,9 @@ NSString *const TGAttachmentVideoCellIdentifier = @"AttachmentVideoCell";
- (void)layoutSubviews
{
[super layoutSubviews];
_iconView.frame = CGRectMake(0, self.frame.size.height - 19, 19, 19);
CGSize durationSize = _durationLabel.frame.size;
_durationLabel.frame = CGRectMake(self.frame.size.width - durationSize.width - 4, self.frame.size.height - durationSize.height - 2, durationSize.width, durationSize.height);
_durationLabel.frame = CGRectMake(self.frame.size.width - durationSize.width - 3.0, self.frame.size.height - durationSize.height - 1.0, durationSize.width, durationSize.height);
}
- (UIImage *)transitionImageSquared

View File

@ -143,7 +143,7 @@
};
[itemViews addObject:carouselItem];
TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:_signup ? TGLocalized(@"Common.ChoosePhoto") : TGLocalized(@"AttachmentMenu.PhotoOrVideo") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.OpenGallery") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
{
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
@ -160,7 +160,7 @@
if (_hasSearchButton)
{
TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.WebSearch") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
{
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)

View File

@ -552,7 +552,7 @@
if (strongSelf->_dismissed)
return;
[strongSelf setProgressVisible:progressVisible value:progress animated:true];
[strongSelf setProgressVisible:progressVisible value:progress animated:progressVisible];
[strongSelf updateDoneButtonEnabled:doneEnabled animated:true];
if (progressVisible)
strongSelf->_hadProgress = true;

View File

@ -28,7 +28,7 @@ private func extractAnchor(string: String) -> (String, String?) {
private let refreshTimeout: Int32 = 60 * 60 * 12
func cachedFaqInstantPage(context: AccountContext) -> Signal<ResolvedUrl, NoError> {
public func cachedFaqInstantPage(context: AccountContext) -> Signal<ResolvedUrl, NoError> {
var faqUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general-questions"
@ -71,7 +71,7 @@ func cachedFaqInstantPage(context: AccountContext) -> Signal<ResolvedUrl, NoErro
}
}
func faqSearchableItems(context: AccountContext) -> Signal<[SettingsSearchableItem], NoError> {
func faqSearchableItems(context: AccountContext, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
return cachedFaqInstantPage(context: context)
|> map { resolvedUrl -> [SettingsSearchableItem] in
@ -105,7 +105,7 @@ func faqSearchableItems(context: AccountContext) -> Signal<[SettingsSearchableIt
if case let .text(itemText, _) = item, case let .url(text, url, _) = itemText {
let (_, anchor) = extractAnchor(string: url)
var index = nextIndex
if anchor?.contains("delete-my-account") ?? false {
if suggestAccountDeletion && (anchor?.contains("delete-my-account") ?? false) {
index = 1
} else {
nextIndex += 1

View File

@ -84,13 +84,13 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode {
}
}
final class ChangePhoneNumberIntroController: ViewController {
public final class ChangePhoneNumberIntroController: ViewController {
private let context: AccountContext
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
init(context: AccountContext, phoneNumber: String) {
public init(context: AccountContext, phoneNumber: String) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -110,7 +110,7 @@ final class ChangePhoneNumberIntroController: ViewController {
fatalError("init(coder:) has not been implemented")
}
override func loadDisplayNode() {
public override func loadDisplayNode() {
self.displayNode = ChangePhoneNumberIntroControllerNode(presentationData: self.presentationData)
(self.displayNode as! ChangePhoneNumberIntroControllerNode).dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
@ -121,7 +121,7 @@ final class ChangePhoneNumberIntroController: ViewController {
self.displayNodeDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
/*if !self.didPlayPresentationAnimation {

View File

@ -57,14 +57,14 @@ private enum DataAndStorageSection: Int32 {
case enableSensitiveContent
}
enum DataAndStorageEntryTag: ItemListItemTag {
public enum DataAndStorageEntryTag: ItemListItemTag {
case automaticDownloadReset
case autoplayGifs
case autoplayVideos
case saveEditedPhotos
case downloadInBackground
func isEqual(to other: ItemListItemTag) -> Bool {
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DataAndStorageEntryTag, self == other {
return true
} else {
@ -518,7 +518,7 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
return entries
}
func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndStorageEntryTag? = nil) -> ViewController {
public func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndStorageEntryTag? = nil) -> ViewController {
let initialState = DataAndStorageControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)

View File

@ -129,7 +129,7 @@ private func logoutOptionsEntries(presentationData: PresentationData, canAddAcco
return entries
}
func logoutOptionsController(context: AccountContext, navigationController: NavigationController, canAddAccounts: Bool, phoneNumber: String) -> ViewController {
public func logoutOptionsController(context: AccountContext, navigationController: NavigationController, canAddAccounts: Bool, phoneNumber: String) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)?
@ -250,6 +250,7 @@ func logoutOptionsController(context: AccountContext, navigationController: Navi
}
let controller = ItemListController(context: context, state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
pushControllerImpl = { [weak navigationController] value in
navigationController?.pushViewController(value, animated: false)
}

View File

@ -258,19 +258,33 @@ private enum SettingsSearchRecentEntryStableId: Hashable {
}
private enum SettingsSearchRecentEntry: Comparable, Identifiable {
case recent(Int, SettingsSearchableItem)
case recent(Int, SettingsSearchableItem, ChatListSearchItemHeader)
case faq(Int, SettingsSearchableItem, ChatListSearchItemHeader)
var stableId: SettingsSearchRecentEntryStableId {
switch self {
case let .recent(_, item):
case let .recent(_, item, _), let .faq(_, item, _):
return .recent(item.id)
}
}
var header: ChatListSearchItemHeader {
switch self {
case let .recent(_, _, header), let .faq(_, _, header):
return header
}
}
static func ==(lhs: SettingsSearchRecentEntry, rhs: SettingsSearchRecentEntry) -> Bool {
switch lhs {
case let .recent(lhsIndex, lhsItem):
if case let .recent(rhsIndex, rhsItem) = rhs, lhsIndex == rhsIndex, lhsItem.id == rhsItem.id {
case let .recent(lhsIndex, lhsItem, lhsHeader):
if case let .recent(rhsIndex, rhsItem, rhsHeader) = rhs, lhsIndex == rhsIndex, lhsItem.id == rhsItem.id, lhsHeader.id == rhsHeader.id {
return true
} else {
return false
}
case let .faq(lhsIndex, lhsItem, lhsHeader):
if case let .faq(rhsIndex, rhsItem, rhsHeader) = rhs, lhsIndex == rhsIndex, lhsItem.id == rhsItem.id, lhsHeader.id == rhsHeader.id {
return true
} else {
return false
@ -280,17 +294,26 @@ private enum SettingsSearchRecentEntry: Comparable, Identifiable {
static func <(lhs: SettingsSearchRecentEntry, rhs: SettingsSearchRecentEntry) -> Bool {
switch lhs {
case let .recent(lhsIndex, _):
case let .recent(lhsIndex, _, _):
switch rhs {
case let .recent(rhsIndex, _):
case let .recent(rhsIndex, _, _):
return lhsIndex <= rhsIndex
case .faq:
return false
}
case let .faq(lhsIndex, _, _):
switch rhs {
case .recent:
return true
case let .faq(rhsIndex, _, _):
return lhsIndex <= rhsIndex
}
}
}
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction, header: ListViewItemHeader) -> ListViewItem {
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> ListViewItem {
switch self {
case let .recent(_, item):
case let .recent(_, item, header), let .faq(_, item, header):
return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: item.title, breadcrumbs: item.breadcrumbs, action: {
interaction.openItem(item)
}, deleted: {
@ -307,18 +330,18 @@ private struct SettingsSearchContainerRecentTransition {
let isEmpty: Bool
}
private func preparedSettingsSearchContainerRecentTransition(from fromEntries: [SettingsSearchRecentEntry], to toEntries: [SettingsSearchRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction, header: ListViewItemHeader) -> SettingsSearchContainerRecentTransition {
private func preparedSettingsSearchContainerRecentTransition(from fromEntries: [SettingsSearchRecentEntry], to toEntries: [SettingsSearchRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> SettingsSearchContainerRecentTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
return SettingsSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: toEntries.isEmpty)
}
private final class SettingsSearchContainerNode: SearchDisplayControllerContentNode {
public final class SettingsSearchContainerNode: SearchDisplayControllerContentNode {
private let listNode: ListView
private let recentListNode: ListView
@ -335,7 +358,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasWallet: Signal<Bool, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
public init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasWallet: Signal<Bool, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise = Promise(self.presentationData)
@ -354,14 +377,20 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN
self.addSubnode(self.recentListNode)
self.addSubnode(self.listNode)
let interaction = SettingsSearchInteraction(openItem: openResult, deleteRecentItem: { id in
let interaction = SettingsSearchInteraction(openItem: { result in
addRecentSettingsSearchItem(postbox: context.account.postbox, item: result.id)
openResult(result)
}, deleteRecentItem: { id in
removeRecentSettingsSearchItem(postbox: context.account.postbox, item: id)
})
let searchableItems = Promise<[SettingsSearchableItem]>()
searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasWallet: hasWallet, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext))
let queryAndFoundItems = combineLatest(searchableItems.get(), faqSearchableItems(context: context))
let faqItems = Promise<[SettingsSearchableItem]>()
faqItems.set(faqSearchableItems(context: context, suggestAccountDeletion: false))
let queryAndFoundItems = combineLatest(searchableItems.get(), faqSearchableItems(context: context, suggestAccountDeletion: true))
|> mapToSignal { searchableItems, faqSearchableItems -> Signal<(String, [SettingsSearchableItem])?, NoError> in
return self.searchQuery.get()
|> mapToSignal { query -> Signal<(String, [SettingsSearchableItem])?, NoError> in
@ -429,20 +458,26 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN
}
let previousRecentItems = Atomic<[SettingsSearchRecentEntry]?>(value: nil)
self.recentDisposable = (combineLatest(recentSearchItems, self.presentationDataPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] recentSearchItems, presentationData in
self.recentDisposable = (combineLatest(recentSearchItems, faqItems.get(), self.presentationDataPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] recentSearchItems, faqItems, presentationData in
if let strongSelf = self {
var entries: [SettingsSearchRecentEntry] = []
for i in 0 ..< recentSearchItems.count {
entries.append(.recent(i, recentSearchItems[i]))
}
let header = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: {
let recentHeader = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: {
clearRecentSettingsSearchItems(postbox: context.account.postbox)
})
let faqHeader = ChatListSearchItemHeader(type: .faq, theme: presentationData.theme, strings: presentationData.strings)
var entries: [SettingsSearchRecentEntry] = []
for i in 0 ..< recentSearchItems.count {
entries.append(.recent(i, recentSearchItems[i], recentHeader))
}
for i in 0 ..< faqItems.count {
entries.append(.faq(i, faqItems[i], faqHeader))
}
let previousEntries = previousRecentItems.swap(entries)
let transition = preparedSettingsSearchContainerRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, header: header)
let transition = preparedSettingsSearchContainerRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
})
@ -510,7 +545,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN
self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
}
override func searchTextUpdated(text: String) {
public override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
@ -569,7 +604,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
@ -593,7 +628,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN
}
}
override func scrollToTop() {
public override func scrollToTop() {
let listNodeToScroll: ListView
if !self.listNode.isHidden {
listNodeToScroll = self.listNode
@ -657,8 +692,6 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: SettingsSearchContainerNode(context: self.context, openResult: { [weak self] result in
if let strongSelf = self {
addRecentSettingsSearchItem(postbox: strongSelf.context.account.postbox, item: result.id)
result.present(strongSelf.context, strongSelf.getNavigationController?(), { [weak self] mode, controller in
if let strongSelf = self {
switch mode {

View File

@ -40,7 +40,17 @@ class SettingsSearchRecentItem: ListViewItem {
async {
let node = SettingsSearchRecentItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil, !(previousItem is SettingsSearchRecentItem))
var previousHeader: ListViewItemHeader?
if let previousItem = previousItem as? SettingsSearchRecentItem {
previousHeader = previousItem.header
}
var nextHeader: ListViewItemHeader?
if let nextItem = nextItem as? SettingsSearchRecentItem {
nextHeader = nextItem.header
}
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil || nextHeader?.id != self.header?.id, !(previousItem is SettingsSearchRecentItem) || previousHeader?.id != self.header?.id)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
@ -53,7 +63,16 @@ class SettingsSearchRecentItem: ListViewItem {
if let nodeValue = node() as? SettingsSearchRecentItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil, !(previousItem is SettingsSearchRecentItem))
var previousHeader: ListViewItemHeader?
if let previousItem = previousItem as? SettingsSearchRecentItem {
previousHeader = previousItem.header
}
var nextHeader: ListViewItemHeader?
if let nextItem = nextItem as? SettingsSearchRecentItem {
nextHeader = nextItem.header
}
let (nodeLayout, apply) = layout(self, params, nextItem == nil || nextHeader?.id != self.header?.id, !(previousItem is SettingsSearchRecentItem) || previousHeader?.id != self.header?.id)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)

View File

@ -35,7 +35,7 @@ enum SettingsSearchableItemIcon {
case faq
}
enum SettingsSearchableItemId: Hashable {
public enum SettingsSearchableItemId: Hashable {
case profile(Int32)
case proxy(Int32)
case savedMessages(Int32)
@ -152,20 +152,20 @@ enum SettingsSearchableItemId: Hashable {
}
}
enum SettingsSearchableItemPresentation {
public enum SettingsSearchableItemPresentation {
case push
case modal
case immediate
case dismiss
}
struct SettingsSearchableItem {
let id: SettingsSearchableItemId
public struct SettingsSearchableItem {
public let id: SettingsSearchableItemId
let title: String
let alternate: [String]
let icon: SettingsSearchableItemIcon
let breadcrumbs: [String]
let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void
public let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void
}
private func synonyms(_ string: String?) -> [String] {

View File

@ -27,6 +27,8 @@ public struct PresentationResourcesSettings {
public static let proxy = renderIcon(name: "Settings/MenuIcons/Proxy")
public static let savedMessages = renderIcon(name: "Settings/MenuIcons/SavedMessages")
public static let recentCalls = renderIcon(name: "Settings/MenuIcons/RecentCalls")
public static let devices = renderIcon(name: "Settings/MenuIcons/Sessions")
public static let chatFolders = renderIcon(name: "Settings/MenuIcons/ChatListFilters")
public static let stickers = renderIcon(name: "Settings/MenuIcons/Stickers")
public static let notifications = renderIcon(name: "Settings/MenuIcons/Notifications")

View File

@ -205,6 +205,7 @@ framework(
"//submodules/StatisticsUI:StatisticsUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
"//submodules/AuthTransferUI:AuthTransferUI",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -200,6 +200,7 @@ swift_library(
"//submodules/Svg:Svg",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
"//submodules/AuthTransferUI:AuthTransferUI",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

@ -2048,6 +2048,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self?.displayDiceTooltip(dice: dice)
}, animateDiceSuccess: { [weak self] in
self?.chatDisplayNode.animateQuizCorrectOptionSelected()
}, greetingStickerNode: { [weak self] in
return self?.chatDisplayNode.greetingStickerNode
}, requestMessageUpdate: { [weak self] id in
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)

View File

@ -119,6 +119,7 @@ public final class ChatControllerInteraction {
let displayPsa: (String, ASDisplayNode) -> Void
let displayDiceTooltip: (TelegramMediaDice) -> Void
let animateDiceSuccess: () -> Void
let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, () -> Void)?
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -136,7 +137,7 @@ public final class ChatControllerInteraction {
var searchTextHighightState: (String, [MessageIndex])?
var seenOneTimeAnimatedMedia = Set<MessageId>()
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void, updateMessageLike: @escaping (MessageId, Bool) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, NSAttributedString, TextSelectionAction) -> Void, updateMessageLike: @escaping (MessageId, Bool) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping () -> Void, greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, () -> Void)?, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
self.openMessage = openMessage
self.openPeer = openPeer
self.openPeerMention = openPeerMention
@ -199,6 +200,7 @@ public final class ChatControllerInteraction {
self.openMessagePollResults = openMessagePollResults
self.displayDiceTooltip = displayDiceTooltip
self.animateDiceSuccess = animateDiceSuccess
self.greetingStickerNode = greetingStickerNode
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
@ -244,6 +246,8 @@ public final class ChatControllerInteraction {
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: {
}, greetingStickerNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -715,6 +715,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
var greetingStickerNode: (ASDisplayNode, ASDisplayNode, () -> Void)? {
if let greetingStickerNode = self.emptyNode?.greetingStickerNode {
self.historyNode.itemHeaderNodesAlpha = 0.0
return (greetingStickerNode, self, { [weak self] in
self?.historyNode.forEachItemHeaderNode { node in
node.alpha = 1.0
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
})
} else {
return nil
}
}
private var isInFocus: Bool = false
func inFocusUpdated(isInFocus: Bool) {

View File

@ -71,6 +71,16 @@ private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNode
private var didSetupSticker = false
private let disposable = MetaDisposable()
var greetingStickerNode: ASDisplayNode? {
if let animationNode = self.stickerNode.animationNode, animationNode.supernode === stickerNode {
return animationNode
} else if self.stickerNode.imageNode.supernode === stickerNode {
return self.stickerNode.imageNode
} else {
return nil
}
}
init(account: Account, interaction: ChatPanelInterfaceInteraction?) {
self.account = account
self.interaction = interaction
@ -655,7 +665,8 @@ final class ChatEmptyNode: ASDisplayNode {
let node: ASDisplayNode & ChatEmptyNodeContent
switch contentType {
case .regular:
node = ChatEmptyNodeRegularChatContent()
// node = ChatEmptyNodeRegularChatContent()
node = ChatEmptyNodeNearbyChatContent(account: self.account, interaction: self.interaction)
case .secret:
node = ChatEmptyNodeSecretChatContent()
case .group:
@ -668,9 +679,8 @@ final class ChatEmptyNode: ASDisplayNode {
self.content = (contentType, node)
self.addSubnode(node)
contentTransition = .immediate
self.isUserInteractionEnabled = contentType == .peerNearby
}
self.isUserInteractionEnabled = contentType == .peerNearby || contentType == .regular
let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
@ -686,6 +696,15 @@ final class ChatEmptyNode: ASDisplayNode {
transition.updateFrame(node: self.backgroundNode, frame: contentFrame)
}
var greetingStickerNode: ASDisplayNode? {
if let (_, node) = self.content {
if let node = node as? ChatEmptyNodeNearbyChatContent {
return node.greetingStickerNode
}
}
return nil
}
}

View File

@ -173,8 +173,8 @@ final class ChatMediaInputStickerGridItem: GridItem {
final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var currentState: (Account, StickerPackItem, CGSize)?
private var currentSize: CGSize?
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
let imageNode: TransformImageNode
var animationNode: AnimatedStickerNode?
private var placeholderNode: ShimmerEffectNode?
private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem?

View File

@ -39,6 +39,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var animationNode: GenericAnimatedStickerNode?
private var didSetUpAnimationNode = false
private var isPlaying = false
private var animateGreeting = false
private weak var greetingStickerParentNode: ASDisplayNode?
private var greetingCompletion: (() -> Void)?
private var swipeToReplyNode: ChatMessageSwipeToReplyNode?
private var swipeToReplyFeedback: HapticFeedback?
@ -234,22 +237,33 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
self.animationNode = animationNode
} else {
let animationNode = AnimatedStickerNode()
animationNode.started = { [weak self] in
if let strongSelf = self {
strongSelf.imageNode.alpha = 0.0
if let item = strongSelf.item {
if let _ = strongSelf.emojiFile {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
let animationNode: AnimatedStickerNode
if let (node, parentNode, greetingCompletion) = item.controllerInteraction.greetingStickerNode(), let greetingStickerNode = node as? AnimatedStickerNode {
animationNode = greetingStickerNode
self.imageNode.alpha = 0.0
self.animateGreeting = true
self.greetingStickerParentNode = parentNode
self.greetingCompletion = greetingCompletion
self.dateAndStatusNode.alpha = 0.0
} else {
animationNode = AnimatedStickerNode()
animationNode.started = { [weak self] in
if let strongSelf = self {
strongSelf.imageNode.alpha = 0.0
if let item = strongSelf.item {
if let _ = strongSelf.emojiFile {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
}
}
}
}
}
self.animationNode = animationNode
}
if let animationNode = self.animationNode {
if let animationNode = self.animationNode, !self.animateGreeting {
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
}
}
@ -745,8 +759,38 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
strongSelf.imageNode.frame = updatedContentFrame
strongSelf.animationNode?.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
let animationNodeFrame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
if let animationNode = strongSelf.animationNode, let parentNode = strongSelf.greetingStickerParentNode, strongSelf.animateGreeting {
strongSelf.animateGreeting = false
let initialFrame = animationNode.view.convert(animationNode.bounds, to: parentNode.view)
parentNode.addSubnode(animationNode)
animationNode.frame = initialFrame
if true {
let targetScale = animationNodeFrame.width / initialFrame.width
animationNode.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, removeOnCompletion: false)
animationNode.layer.animatePosition(from: initialFrame.center, to: CGPoint(x: animationNodeFrame.midX, y: initialFrame.center.y + 173.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] finished in
if let strongSelf = self {
animationNode.layer.removeAllAnimations()
strongSelf.animationNode?.frame = animationNodeFrame
strongSelf.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: strongSelf.imageNode)
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
}
strongSelf.dateAndStatusNode.alpha = 1.0
strongSelf.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.greetingCompletion?()
}
})
}
} else if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode {
strongSelf.animationNode?.frame = animationNodeFrame
}
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode, strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode {
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
}
imageApply()

View File

@ -427,6 +427,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: {
}, greetingStickerNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -325,7 +325,7 @@ public func createChannelController(context: AccountContext) -> ViewController {
}
}
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: true)!
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in

View File

@ -583,7 +583,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
}
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: true)!
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in

View File

@ -141,6 +141,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: {
}, greetingStickerNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -128,6 +128,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: {
}, greetingStickerNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))

View File

@ -7,16 +7,25 @@ enum PeerInfoScreenActionColor {
case destructive
}
enum PeerInfoScreenActionAligmnent {
case natural
case center
}
final class PeerInfoScreenActionItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let color: PeerInfoScreenActionColor
let icon: UIImage?
let alignment: PeerInfoScreenActionAligmnent
let action: (() -> Void)?
init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, action: (() -> Void)?) {
init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, icon: UIImage? = nil, alignment: PeerInfoScreenActionAligmnent = .natural, action: (() -> Void)?) {
self.id = id
self.text = text
self.color = color
self.icon = icon
self.alignment = alignment
self.action = action
}
@ -27,6 +36,7 @@ final class PeerInfoScreenActionItem: PeerInfoScreenItem {
private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let iconNode: ASImageNode
private let textNode: ImmediateTextNode
private let bottomSeparatorNode: ASDisplayNode
@ -36,6 +46,10 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
@ -54,7 +68,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.textNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenActionItem else {
return 10.0
}
@ -63,7 +77,10 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
let leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0)
let rightInset = sideInset
let separatorInset = item.icon == nil ? sideInset : leftInset - 1.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -78,11 +95,23 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: width - (leftInset + rightInset), height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: item.alignment == .center ? floorToScreenPixels((width - textSize.width) / 2.0) : leftInset, y: 12.0), size: textSize)
let height = textSize.height + 22.0
let height = textSize.height + 24.0
if let icon = item.icon {
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
self.iconNode.image = generateTintedImage(image: icon, color: textColorValue)
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
} else if self.iconNode.supernode != nil {
self.iconNode.image = nil
self.iconNode.removeFromSupernode()
}
transition.updateFrame(node: self.textNode, frame: textFrame)
@ -90,7 +119,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height

View File

@ -63,7 +63,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.selectionNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenAddressItem else {
return 10.0
}
@ -72,13 +72,13 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let addressItem = ItemListAddressItem(theme: presentationData.theme, label: item.label, text: item.text, imageSignal: item.imageSignal, sectionId: 0, style: .blocks, displayDecorations: false, action: nil, longTapAction: item.longTapAction, linkItemAction: item.linkItemAction)
let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListAddressItemNode
if let current = self.itemNode {

View File

@ -52,7 +52,7 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.selectionNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenCallListItem else {
return 10.0
}
@ -61,13 +61,13 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
self.selectionNode.pressed = nil
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let addressItem = ItemListCallListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, messages: item.messages, sectionId: 0, style: .blocks, displayDecorations: false)
let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListCallListItemNode
if let current = self.itemNode {

View File

@ -31,14 +31,14 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.textNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, 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 sideInset: CGFloat = 16.0 + safeInsets.left
let verticalInset: CGFloat = 7.0
self.textNode.maximumNumberOfLines = 0

View File

@ -66,7 +66,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree
self.addSubnode(self.arrowNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else {
return 10.0
}
@ -79,7 +79,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -90,9 +90,9 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 12.0), size: textSize)
let height = textSize.height + 22.0
let height = textSize.height + 24.0
if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) {
self.arrowNode.image = arrowImage

View File

@ -3,15 +3,41 @@ import Display
import TelegramPresentationData
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
enum Label {
case none
case text(String)
case badge(String, UIColor)
var text: String {
switch self {
case .none:
return ""
case let .text(text), let .badge(text, _):
return text
}
}
var badgeColor: UIColor? {
switch self {
case .none, .text:
return nil
case let .badge(_, color):
return color
}
}
}
let id: AnyHashable
let label: String
let label: Label
let text: String
let icon: UIImage?
let action: (() -> Void)?
init(id: AnyHashable, label: String, text: String, action: (() -> Void)?) {
init(id: AnyHashable, label: Label = .none, text: String, icon: UIImage? = nil, action: (() -> Void)?) {
self.id = id
self.label = label
self.text = text
self.icon = icon
self.action = action
}
@ -22,6 +48,8 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let iconNode: ASImageNode
private let labelBadgeNode: ASImageNode
private let labelNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let arrowNode: ASImageNode
@ -33,6 +61,15 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.labelBadgeNode = ASImageNode()
self.labelBadgeNode.displayWithoutProcessing = true
self.labelBadgeNode.displaysAsynchronously = false
self.labelBadgeNode.isLayerBacked = true
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
@ -63,36 +100,56 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.arrowNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureItem else {
return 10.0
}
let previousItem = self.item
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
let leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0)
let rightInset = sideInset + 18.0
let separatorInset = item.icon == nil ? sideInset : leftInset - 1.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor
let labelColorValue: UIColor = presentationData.theme.list.itemSecondaryTextColor
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(17.0), textColor: labelColorValue)
let labelColorValue: UIColor
let labelFont: UIFont
if case .badge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.regular(15.0)
} else {
labelColorValue = presentationData.theme.list.itemSecondaryTextColor
labelFont = Font.regular(17.0)
}
self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue)
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let labelSize = self.labelNode.updateLayout(CGSize(width: width - textSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: width - (leftInset + rightInset), height: .greatestFiniteMagnitude))
let labelSize = self.labelNode.updateLayout(CGSize(width: width - textSize.width - (leftInset + rightInset), height: .greatestFiniteMagnitude))
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let labelFrame = CGRect(origin: CGPoint(x: width - sideInset - arrowInset - labelSize.width, y: 11.0), size: labelSize)
let height = textSize.height + 24.0
let height = textSize.height + 22.0
if let icon = item.icon {
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
self.iconNode.image = icon
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
} else if self.iconNode.supernode != nil {
self.iconNode.image = nil
self.iconNode.removeFromSupernode()
}
if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) {
self.arrowNode.image = arrowImage
@ -100,6 +157,29 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let badgeDiameter: CGFloat = 20.0
if case let .badge(text, badgeColor) = item.label, !text.isEmpty {
if previousItem?.label.badgeColor != badgeColor {
self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else {
self.labelBadgeNode.removeFromSupernode()
}
let badgeWidth = max(badgeDiameter, labelSize.width + 10.0)
let labelFrame: CGRect
if case .badge = item.label {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
} else {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize)
}
let labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter))
transition.updateFrame(node: self.labelBadgeNode, frame: labelBadgeNodeFrame)
transition.updateFrame(node: self.labelNode, frame: labelFrame)
transition.updateFrame(node: self.textNode, frame: textFrame)
@ -107,7 +187,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height

View File

@ -31,14 +31,14 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.textNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenHeaderItem else {
return 10.0
}
self.item = item
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
let verticalInset: CGFloat = 7.0
self.textNode.maximumNumberOfLines = 0

View File

@ -178,7 +178,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenLabeledValueItem else {
return 10.0
}
@ -188,7 +188,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor

View File

@ -21,22 +21,28 @@ enum PeerInfoScreenMemberItemAction {
final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
let id: AnyHashable
let context: AccountContext
let enclosingPeer: Peer
let enclosingPeer: Peer?
let member: PeerInfoMember
let badge: String?
let action: ((PeerInfoScreenMemberItemAction) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init(
id: AnyHashable,
context: AccountContext,
enclosingPeer: Peer,
enclosingPeer: Peer?,
member: PeerInfoMember,
action: ((PeerInfoScreenMemberItemAction) -> Void)?
badge: String? = nil,
action: ((PeerInfoScreenMemberItemAction) -> Void)?,
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil
) {
self.id = id
self.context = context
self.enclosingPeer = enclosingPeer
self.member = member
self.badge = badge
self.action = action
self.contextAction = contextAction
}
func node() -> PeerInfoScreenItemNode {
@ -103,7 +109,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenMemberItem else {
return 10.0
}
@ -116,7 +122,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
}
}
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -152,14 +158,38 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
item.action?(.remove)
}))
}
if actions.contains(.logout) {
options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Settings_Context_Logout, action: {
item.action?(.remove)
}))
}
let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.member.peer, height: .peerList, presence: item.member.presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
let itemLabel: ItemListPeerItemLabel
if let label = label {
itemLabel = .text(label, .standard)
} else if let badge = item.badge {
itemLabel = .badge(badge)
} else {
itemLabel = .none
}
let itemHeight: ItemListPeerItemHeight
let itemText: ItemListPeerItemText
if case .account = item.member {
itemHeight = .generic
itemText = .none
} else {
itemHeight = .peerList
itemText = .presence
}
let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.member.peer, height: itemHeight, presence: item.member.presence, text: itemText, label: itemLabel, editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
}, removePeer: { _ in
}, contextAction: nil, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, displayDecorations: false)
}, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, displayDecorations: false)
let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListPeerItemNode
if let current = self.itemNode {

View File

@ -59,7 +59,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenSwitchItem else {
return 10.0
}
@ -78,7 +78,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
self.selectionNode.pressed = nil
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -91,9 +91,9 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 12.0), size: textSize)
let height = textSize.height + 22.0
let height = textSize.height + 24.0
transition.updateFrame(node: self.textNode, frame: textFrame)

View File

@ -9,6 +9,10 @@ import PeerPresenceStatusManager
import TelegramStringFormatting
import TelegramPresentationData
import PeerAvatarGalleryUI
import TelegramUIPreferences
import TelegramNotices
import AccountUtils
import DeviceAccess
enum PeerInfoUpdatingAvatar {
case none
@ -19,22 +23,26 @@ final class PeerInfoState {
let isEditing: Bool
let selectedMessageIds: Set<MessageId>?
let updatingAvatar: PeerInfoUpdatingAvatar?
let updatingBio: String?
init(
isEditing: Bool,
selectedMessageIds: Set<MessageId>?,
updatingAvatar: PeerInfoUpdatingAvatar?
updatingAvatar: PeerInfoUpdatingAvatar?,
updatingBio: String?
) {
self.isEditing = isEditing
self.selectedMessageIds = selectedMessageIds
self.updatingAvatar = updatingAvatar
self.updatingBio = updatingBio
}
func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
return PeerInfoState(
isEditing: isEditing,
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio
)
}
@ -42,7 +50,8 @@ final class PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
selectedMessageIds: selectedMessageIds,
updatingAvatar: self.updatingAvatar
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio
)
}
@ -50,9 +59,74 @@ final class PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: updatingAvatar
updatingAvatar: updatingAvatar,
updatingBio: self.updatingBio
)
}
func withUpdatingBio(_ updatingBio: String?) -> PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: updatingBio
)
}
}
final class TelegramGlobalSettings {
let suggestPhoneNumberConfirmation: Bool
let accountsAndPeers: [(Account, Peer, Int32)]
let activeSessionsContext: ActiveSessionsContext
let webSessionsContext: WebSessionsContext
let otherSessionsCount: Int
let proxySettings: ProxySettings
let notificationAuthorizationStatus: AccessType
let notificationWarningSuppressed: Bool
let notificationExceptions: NotificationExceptionsList?
let inAppNotificationSettings: InAppNotificationSettings
let privacySettings: AccountPrivacySettings?
let unreadTrendingStickerPacks: Int
let archivedStickerPacks: [ArchivedStickerPackItem]?
let hasPassport: Bool
let hasWatchApp: Bool
let enableQRLogin: Bool
init(
suggestPhoneNumberConfirmation: Bool,
accountsAndPeers: [(Account, Peer, Int32)],
activeSessionsContext: ActiveSessionsContext,
webSessionsContext: WebSessionsContext,
otherSessionsCount: Int,
proxySettings: ProxySettings,
notificationAuthorizationStatus: AccessType,
notificationWarningSuppressed: Bool,
notificationExceptions: NotificationExceptionsList?,
inAppNotificationSettings: InAppNotificationSettings,
privacySettings: AccountPrivacySettings?,
unreadTrendingStickerPacks: Int,
archivedStickerPacks: [ArchivedStickerPackItem]?,
hasPassport: Bool,
hasWatchApp: Bool,
enableQRLogin: Bool
) {
self.suggestPhoneNumberConfirmation = suggestPhoneNumberConfirmation
self.accountsAndPeers = accountsAndPeers
self.activeSessionsContext = activeSessionsContext
self.webSessionsContext = webSessionsContext
self.otherSessionsCount = otherSessionsCount
self.proxySettings = proxySettings
self.notificationAuthorizationStatus = notificationAuthorizationStatus
self.notificationWarningSuppressed = notificationWarningSuppressed
self.notificationExceptions = notificationExceptions
self.inAppNotificationSettings = inAppNotificationSettings
self.privacySettings = privacySettings
self.unreadTrendingStickerPacks = unreadTrendingStickerPacks
self.archivedStickerPacks = archivedStickerPacks
self.hasPassport = hasPassport
self.hasWatchApp = hasWatchApp
self.enableQRLogin = enableQRLogin
}
}
final class PeerInfoScreenData {
@ -67,6 +141,7 @@ final class PeerInfoScreenData {
let linkedDiscussionPeer: Peer?
let members: PeerInfoMembersData?
let encryptionKeyFingerprint: SecretChatKeyFingerprint?
let globalSettings: TelegramGlobalSettings?
init(
peer: Peer?,
@ -79,7 +154,8 @@ final class PeerInfoScreenData {
groupsInCommon: GroupsInCommonContext?,
linkedDiscussionPeer: Peer?,
members: PeerInfoMembersData?,
encryptionKeyFingerprint: SecretChatKeyFingerprint?
encryptionKeyFingerprint: SecretChatKeyFingerprint?,
globalSettings: TelegramGlobalSettings?
) {
self.peer = peer
self.cachedData = cachedData
@ -92,6 +168,7 @@ final class PeerInfoScreenData {
self.linkedDiscussionPeer = linkedDiscussionPeer
self.members = members
self.encryptionKeyFingerprint = encryptionKeyFingerprint
self.globalSettings = globalSettings
}
}
@ -99,10 +176,12 @@ private enum PeerInfoScreenInputUserKind {
case user
case bot
case support
case settings
}
private enum PeerInfoScreenInputData: Equatable {
case none
case settings
case user(userId: PeerId, secretChatId: PeerId?, kind: PeerInfoScreenInputUserKind)
case channel
case group(groupId: PeerId)
@ -183,22 +262,26 @@ enum PeerInfoMembersData: Equatable {
}
}
private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId) -> Signal<PeerInfoScreenInputData, NoError> {
private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId, isSettings: Bool) -> Signal<PeerInfoScreenInputData, NoError> {
return context.account.postbox.combinedView(keys: [.basicPeer(peerId)])
|> map { view -> PeerInfoScreenInputData in
guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else {
return .none
}
if let user = peer as? TelegramUser {
let kind: PeerInfoScreenInputUserKind
if user.flags.contains(.isSupport) {
kind = .support
} else if user.botInfo != nil {
kind = .bot
if isSettings && user.id == context.account.peerId {
return .settings
} else {
kind = .user
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)
}
return .user(userId: user.id, secretChatId: nil, kind: kind)
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return .group(groupId: channel.id)
@ -248,10 +331,10 @@ func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) ->
}
func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signal<Never, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId)
return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: false)
|> mapToSignal { inputData -> Signal<Never, NoError> in
switch inputData {
case .none:
case .none, .settings:
return .complete()
case .user, .channel, .group:
return combineLatest(
@ -263,11 +346,92 @@ func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signa
}
}
func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, ignoreGroupInCommon: PeerId?) -> Signal<PeerInfoScreenData, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId)
func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, accountsAndPeers: Signal<[(Account, Peer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError>, notificationExceptions: Signal<NotificationExceptionsList?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal<Bool, NoError>) -> Signal<PeerInfoScreenData, NoError> {
let preferences = context.sharedContext.accountManager.sharedData(keys: [
SharedDataKeys.proxySettings,
ApplicationSpecificSharedDataKeys.inAppNotificationSettings,
ApplicationSpecificSharedDataKeys.experimentalUISettings
])
let notificationsAuthorizationStatus = Promise<AccessType>(.allowed)
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
notificationsAuthorizationStatus.set(
.single(.allowed)
|> then(DeviceAccess.authorizationStatus(applicationInForeground: context.sharedContext.applicationBindings.applicationInForeground, subject: .notifications)
)
)
}
let notificationsWarningSuppressed = Promise<Bool>(true)
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
notificationsWarningSuppressed.set(
.single(true)
|> then(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .notifications)!)
|> map { noticeView -> Bool in
let timestamp = noticeView.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) })
if let timestamp = timestamp, timestamp > 0 {
return true
} else {
return false
}
}
)
)
}
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
accountsAndPeers,
activeSessionsContextAndCount,
privacySettings,
preferences,
combineLatest(notificationExceptions, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get()),
combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks),
hasPassport,
(context.watchManager?.watchAppInstalled ?? .single(false)),
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
)
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences -> PeerInfoScreenData in
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
let proxySettings: ProxySettings = sharedPreferences.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings
let inAppNotificationSettings: InAppNotificationSettings = sharedPreferences.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings
let experimentalUISettings: ExperimentalUISettings = sharedPreferences.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings
let unreadTrendingStickerPacks = featuredStickerPacks.reduce(0, { count, item -> Int in
return item.unread ? count + 1 : count
})
var enableQRLogin = false
if let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration] as? AppConfiguration, let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR {
enableQRLogin = true
}
let globalSettings = TelegramGlobalSettings(suggestPhoneNumberConfirmation: false, accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions.0, webSessionsContext: accountSessions.2, otherSessionsCount: accountSessions.1, proxySettings: proxySettings, notificationAuthorizationStatus: notificationsAuthorizationStatus, notificationWarningSuppressed: notificationsWarningSuppressed, notificationExceptions: notificationExceptions, inAppNotificationSettings: inAppNotificationSettings, privacySettings: privacySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin)
return PeerInfoScreenData(
peer: peerView.peers[peerId],
cachedData: peerView.cachedData,
status: nil,
notificationSettings: nil,
globalNotificationSettings: nil,
isContact: false,
availablePanes: [],
groupsInCommon: nil,
linkedDiscussionPeer: nil,
members: nil,
encryptionKeyFingerprint: nil,
globalSettings: globalSettings
)
}
}
func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, ignoreGroupInCommon: PeerId?) -> Signal<PeerInfoScreenData, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: isSettings)
|> mapToSignal { inputData -> Signal<PeerInfoScreenData, NoError> in
switch inputData {
case .none:
case .none, .settings:
return .single(PeerInfoScreenData(
peer: nil,
cachedData: nil,
@ -279,7 +443,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
groupsInCommon: nil,
linkedDiscussionPeer: nil,
members: nil,
encryptionKeyFingerprint: nil
encryptionKeyFingerprint: nil,
globalSettings: nil
))
case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext?
@ -417,7 +582,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
groupsInCommon: groupsInCommon,
linkedDiscussionPeer: nil,
members: nil,
encryptionKeyFingerprint: encryptionKeyFingerprint
encryptionKeyFingerprint: encryptionKeyFingerprint,
globalSettings: nil
)
}
case .channel:
@ -467,7 +633,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
groupsInCommon: nil,
linkedDiscussionPeer: discussionPeer,
members: nil,
encryptionKeyFingerprint: nil
encryptionKeyFingerprint: nil,
globalSettings: nil
)
}
case let .group(groupId):
@ -604,14 +771,18 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
groupsInCommon: nil,
linkedDiscussionPeer: discussionPeer,
members: membersData,
encryptionKeyFingerprint: nil
encryptionKeyFingerprint: nil,
globalSettings: nil
)
}
}
}
}
func canEditPeerInfo(peer: Peer?) -> Bool {
func canEditPeerInfo(context: AccountContext, peer: Peer?) -> Bool {
if context.account.peerId == peer?.id {
return true
}
if let channel = peer as? TelegramChannel {
if channel.hasPermission(.changeInfo) {
return true
@ -639,12 +810,15 @@ struct PeerInfoMemberActions: OptionSet {
static let restrict = PeerInfoMemberActions(rawValue: 1 << 0)
static let promote = PeerInfoMemberActions(rawValue: 1 << 1)
static let logout = PeerInfoMemberActions(rawValue: 1 << 2)
}
func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member: PeerInfoMember) -> PeerInfoMemberActions {
func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: PeerInfoMember) -> PeerInfoMemberActions {
var result: PeerInfoMemberActions = []
if member.id != accountPeerId {
if peer == nil {
result.insert(.logout)
} else if member.id != accountPeerId {
if let channel = peer as? TelegramChannel {
if channel.flags.contains(.isCreator) {
result.insert(.restrict)
@ -671,6 +845,8 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member:
}
case .legacyGroupMember:
break
case .account:
break
}
}
} else if let group = peer as? TelegramGroup {
@ -687,6 +863,8 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member:
}
case .channelMember:
break
case .account:
break
}
case .member:
switch member {
@ -696,6 +874,8 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member:
}
case .channelMember:
break
case .account:
break
}
}
}

View File

@ -12,6 +12,7 @@ import TelegramPresentationData
import PhotoResources
import PeerAvatarGalleryUI
import TelegramStringFormatting
import PhoneNumberFormat
import ActivityIndicator
import TelegramUniversalVideoContent
import GalleryUI
@ -180,6 +181,8 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode {
let isReady = Promise<Bool>()
private var didSetReady: Bool = false
var item: PeerInfoAvatarListItem?
var isExpanded: Bool = false {
didSet {
if let videoNode = self.videoNode, videoNode.canAttachContent != self.isExpanded {
@ -190,7 +193,7 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode {
}
}
}
var isCentral: Bool = false
init(context: AccountContext) {
@ -229,6 +232,8 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode {
}
func setup(item: PeerInfoAvatarListItem, synchronous: Bool) {
self.item = item
let representations: [ImageRepresentationWithReference]
let videoRepresentations: [TelegramMediaImage.VideoRepresentation]
let immediateThumbnailData: Data?
@ -339,7 +344,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode {
private var stripNodes: [ASImageNode] = []
private let activeStripImage: UIImage
private var appliedStripNodeCurrentIndex: Int?
private var currentIndex: Int = 0
var currentIndex: Int = 0
private var transitionFraction: CGFloat = 0.0
private var validLayout: CGSize?
@ -970,7 +975,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
func update(peer: Peer?, updatingAvatar: PeerInfoUpdatingAvatar?, theme: PresentationTheme, avatarSize: CGFloat) {
if let peer = peer {
let overrideImage: AvatarNodeImageOverride?
if canEditPeerInfo(peer: peer) {
if canEditPeerInfo(context: self.context, peer: peer) {
if let updatingAvatar = updatingAvatar {
switch updatingAvatar {
case let .image(representation):
@ -1174,6 +1179,8 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
case .search:
text = ""
icon = PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme)
case .editPhoto:
text = presentationData.strings.Settings_EditPhoto
}
let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0)
@ -1211,6 +1218,7 @@ enum PeerInfoHeaderNavigationButtonKey {
case select
case selectionDone
case search
case editPhoto
}
struct PeerInfoHeaderNavigationButtonSpec: Equatable {
@ -1627,6 +1635,7 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
private let requestUpdateLayout: () -> Void
let avatarNode: PeerInfoEditingAvatarNode
let avatarTextNode: HighlightableButtonNode
var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:]
@ -1636,9 +1645,17 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
self.avatarNode = PeerInfoEditingAvatarNode(context: context)
self.avatarTextNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.avatarNode)
self.avatarTextNode.addTarget(self, action: #selector(textPressed), forControlEvents: .touchUpInside)
}
@objc private func textPressed() {
self.avatarNode.tapped?()
}
func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? {
@ -1649,13 +1666,25 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
self.itemNodes[key]?.layer.addShakeAnimation()
}
func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, cachedData: CachedPeerData?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
let avatarSize: CGFloat = isModalOverlay ? 200.0 : 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()))
var contentHeight: CGFloat = statusBarHeight + 10.0 + avatarSize + 20.0
if isSettings {
if self.avatarTextNode.supernode == nil {
self.addSubnode(self.avatarTextNode)
}
self.avatarTextNode.setAttributedTitle(NSAttributedString(string: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor), for: [])
let avatarTextSize = self.avatarTextNode.measure(CGSize(width: width, height: 32.0))
transition.updateFrame(node: self.avatarTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - avatarTextSize.width) / 2.0), y: contentHeight - 1.0), size: avatarTextSize))
contentHeight += 32.0
}
var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = []
if let user = peer as? TelegramUser {
if !user.isDeleted {
@ -1666,12 +1695,12 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
}
} else if let _ = peer as? TelegramGroup {
fieldKeys.append(.title)
if canEditPeerInfo(peer: peer) {
if canEditPeerInfo(context: self.context, peer: peer) {
fieldKeys.append(.description)
}
} else if let _ = peer as? TelegramChannel {
fieldKeys.append(.title)
if canEditPeerInfo(peer: peer) {
if canEditPeerInfo(context: self.context, peer: peer) {
fieldKeys.append(.description)
}
}
@ -1715,20 +1744,20 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
switch key {
case .firstName:
placeholder = presentationData.strings.UserInfo_FirstNamePlaceholder
isEnabled = isContact
isEnabled = isContact || isSettings
case .lastName:
placeholder = presentationData.strings.UserInfo_LastNamePlaceholder
isEnabled = isContact
isEnabled = isContact || isSettings
case .title:
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder
} else {
placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder
}
isEnabled = canEditPeerInfo(peer: peer)
isEnabled = canEditPeerInfo(context: self.context, peer: peer)
case .description:
placeholder = presentationData.strings.Channel_Edit_AboutItem
isEnabled = canEditPeerInfo(peer: peer)
isEnabled = canEditPeerInfo(context: self.context, 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)))
@ -1759,6 +1788,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
private var presentationData: PresentationData?
private let isOpenedFromChat: Bool
private let isSettings: Bool
private let videoCallsEnabled: Bool
private(set) var isAvatarExpanded: Bool
@ -1775,6 +1805,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let subtitleNodeContainer: ASDisplayNode
let subtitleNodeRawContainer: ASDisplayNode
let subtitleNode: MultiScaleTextNode
let usernameNodeContainer: ASDisplayNode
let usernameNodeRawContainer: ASDisplayNode
let usernameNode: MultiScaleTextNode
private var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:]
private let backgroundNode: ASDisplayNode
private let expandedBackgroundNode: ASDisplayNode
@ -1788,10 +1821,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var navigationTransition: PeerInfoHeaderNavigationTransition?
init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool) {
init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isSettings: Bool) {
self.context = context
self.isAvatarExpanded = avatarInitiallyExpanded
self.isOpenedFromChat = isOpenedFromChat
self.isSettings = isSettings
self.videoCallsEnabled = context.sharedContext.immediateExperimentalUISettings.videoCalls
self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded)
@ -1816,6 +1850,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.subtitleNode.displaysAsynchronously = false
self.usernameNodeContainer = ASDisplayNode()
self.usernameNodeRawContainer = ASDisplayNode()
self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded])
self.usernameNode.displaysAsynchronously = false
self.regularContentNode = PeerInfoHeaderRegularContentNode()
var requestUpdateLayoutImpl: (() -> Void)?
self.editingContentNode = PeerInfoHeaderEditingContentNode(context: context, requestUpdateLayout: {
@ -1846,6 +1885,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.regularContentNode.addSubnode(self.titleNodeContainer)
self.subtitleNodeContainer.addSubnode(self.subtitleNode)
self.regularContentNode.addSubnode(self.subtitleNodeContainer)
self.usernameNodeContainer.addSubnode(self.usernameNode)
self.regularContentNode.addSubnode(self.usernameNodeContainer)
self.regularContentNode.addSubnode(self.avatarListNode)
self.regularContentNode.addSubnode(self.avatarListNode.listContainerNode.controlsClippingOffsetNode)
self.addSubnode(self.regularContentNode)
@ -1910,7 +1951,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry)
}
func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isContact: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat {
func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isContact: Bool, isSettings: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat {
var contentOffset = contentOffset
if isMediaOnly {
@ -1942,7 +1983,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, isModalOverlay: isModalOverlay, peer: state.isEditing ? peer : nil, cachedData: cachedData, isContact: isContact, presentationData: presentationData, transition: transition)
let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, isModalOverlay: isModalOverlay, peer: state.isEditing ? peer : nil, cachedData: cachedData, isContact: isContact, isSettings: isSettings, 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
@ -1982,23 +2023,28 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight)
let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight)
let buttonKeys: [PeerInfoHeaderButtonKey] = peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled)
let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled)
var isVerified = false
let titleString: NSAttributedString
let subtitleString: NSAttributedString
let usernameString: NSAttributedString
if let peer = peer, peer.isVerified {
isVerified = true
}
if let peer = peer {
if peer.id == self.context.account.peerId {
if peer.id == self.context.account.peerId && !self.isSettings {
titleString = NSAttributedString(string: presentationData.strings.Conversation_SavedMessages, font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
} else {
titleString = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
}
if let statusData = statusData {
if self.isSettings, let user = peer as? TelegramUser {
let formattedPhone = formatPhoneNumber(user.phone ?? "")
subtitleString = NSAttributedString(string: formattedPhone, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
usernameString = NSAttributedString(string: user.addressName.flatMap { "@\($0)" } ?? "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemAccentColor)
} else if let statusData = statusData {
let subtitleColor: UIColor
if statusData.isActivity {
subtitleColor = presentationData.theme.list.itemAccentColor
@ -2006,12 +2052,15 @@ final class PeerInfoHeaderNode: ASDisplayNode {
subtitleColor = presentationData.theme.list.itemSecondaryTextColor
}
subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: subtitleColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemAccentColor)
} else {
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemAccentColor)
}
} else {
titleString = NSAttributedString(string: " ", font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemAccentColor)
}
let textSideInset: CGFloat = 44.0
@ -2029,6 +2078,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
TitleNodeStateExpanded: MultiScaleTextState(attributedText: subtitleString, constrainedSize: CGSize(width: titleConstrainedSize.width - 82.0, height: titleConstrainedSize.height))
], mainState: TitleNodeStateRegular)
let usernameNodeLayout = self.usernameNode.updateLayout(states: [
TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)),
TitleNodeStateExpanded: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height))
], mainState: TitleNodeStateRegular)
self.titleNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
@ -2039,14 +2093,19 @@ final class PeerInfoHeaderNode: ASDisplayNode {
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], transition: transition)
self.usernameNode.update(stateFractions: [
TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0,
TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0
], transition: transition)
let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0
let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 10.0), size: CGSize(width: avatarSize, height: avatarSize))
let avatarCenter = CGPoint(x: (1.0 - transitionFraction) * avatarFrame.midX + transitionFraction * transitionSourceAvatarFrame.midX, y: (1.0 - transitionFraction) * avatarFrame.midY + transitionFraction * transitionSourceAvatarFrame.midY)
let avatarAlpha = transitionFraction
let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size
let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size
let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size
let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size
if let image = self.titleCredibilityIconNode.image {
transition.updateFrame(node: self.titleCredibilityIconNode, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - image.size.height) / 2.0) + 1.0), size: image.size))
@ -2058,14 +2117,25 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let titleFrame: CGRect
let subtitleFrame: CGRect
let usernameFrame: CGRect
let usernameSpacing: CGFloat = 4.0
if self.isAvatarExpanded {
let minTitleSize = CGSize(width: titleSize.width * 0.7, height: titleSize.height * 0.7)
let minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - expandedAvatarControlsHeight + 9.0 + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize)
titleFrame = CGRect(origin: CGPoint(x: minTitleFrame.midX - titleSize.width / 2.0, y: minTitleFrame.midY - titleSize.height / 2.0), size: titleSize)
subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 4.0), size: subtitleSize)
usernameFrame = CGRect(origin: CGPoint(x: width - usernameSize.width - 16.0, y: minTitleFrame.midY - usernameSize.height / 2.0), size: usernameSize)
} else {
titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarFrame.maxY + 10.0 + (subtitleSize.height.isZero ? 11.0 : 0.0)), size: titleSize)
subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
let totalSubtitleWidth = subtitleSize.width + usernameSpacing + usernameSize.width
if usernameSize.width == 0.0 || totalSubtitleWidth > width - textSideInset * 2.0 {
subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
usernameFrame = CGRect(origin: CGPoint(x: floor((width - usernameSize.width) / 2.0), y: subtitleFrame.maxY + 1.0), size: usernameSize)
} else {
subtitleFrame = CGRect(origin: CGPoint(x: floor((width - totalSubtitleWidth) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
usernameFrame = CGRect(origin: CGPoint(x: subtitleFrame.maxX + usernameSpacing, y: titleFrame.maxY + 1.0), size: usernameSize)
}
}
let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0
@ -2190,7 +2260,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer, isExpanded: self.isAvatarExpanded, transition: transition)
let panelWithAvatarHeight: CGFloat = 112.0 + avatarSize
let panelWithAvatarHeight: CGFloat = (self.isSettings ? 40.0 : 112.0) + avatarSize
let buttonsCollapseStart = titleCollapseOffset
let buttonsCollapseEnd = panelWithAvatarHeight - (navigationHeight - statusBarHeight) + 10.0
@ -2231,8 +2301,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.subtitleNodeRawContainer.frame = rawSubtitleFrame
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale)
transition.updateSublayerTransformScale(node: self.usernameNodeContainer, scale: subtitleScale)
if self.isSettings {
transition.updateAlpha(node: self.subtitleNode, alpha: 1.0 - titleCollapseFraction, beginWithCurrentState: true)
transition.updateAlpha(node: self.usernameNode, alpha: 1.0 - titleCollapseFraction, beginWithCurrentState: true)
}
} else {
let titleScale: CGFloat
let subtitleScale: CGFloat
@ -2249,16 +2325,26 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
let rawSubtitleFrame = subtitleFrame
self.subtitleNodeRawContainer.frame = rawSubtitleFrame
let rawUsernameFrame = usernameFrame
self.usernameNodeRawContainer.frame = rawUsernameFrame
if self.isAvatarExpanded {
transition.updateFrameAdditive(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset))
transition.updateFrameAdditive(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
transition.updateFrameAdditive(node: self.usernameNodeContainer, frame: CGRect(origin: rawUsernameFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
} else {
transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset))
transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: rawUsernameFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset))
}
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize()))
transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale)
transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale)
transition.updateSublayerTransformScaleAdditive(node: self.usernameNodeContainer, scale: subtitleScale)
if self.isSettings {
transition.updateAlpha(node: self.subtitleNode, alpha: 1.0 - titleCollapseFraction, beginWithCurrentState: true)
transition.updateAlpha(node: self.usernameNode, alpha: 1.0 - titleCollapseFraction, beginWithCurrentState: true)
}
}
}
@ -2440,7 +2526,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if self.isAvatarExpanded {
resolvedRegularHeight = expandedAvatarListSize.height + expandedAvatarControlsHeight
} else {
resolvedRegularHeight = 112.0 + avatarSize + navigationHeight
resolvedRegularHeight = (self.isSettings ? 40.0 : 112.0) + avatarSize + navigationHeight
}
let backgroundFrame: CGRect

View File

@ -15,6 +15,7 @@ enum PeerInfoMemberRole {
enum PeerInfoMember: Equatable {
case channelMember(RenderedChannelParticipant)
case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?)
case account(peer: RenderedPeer)
var id: PeerId {
switch self {
@ -22,6 +23,8 @@ enum PeerInfoMember: Equatable {
return channelMember.peer.id
case let .legacyGroupMember(legacyGroupMember):
return legacyGroupMember.peer.peerId
case let .account(peer):
return peer.peerId
}
}
@ -31,6 +34,8 @@ enum PeerInfoMember: Equatable {
return channelMember.peer
case let .legacyGroupMember(legacyGroupMember):
return legacyGroupMember.peer.peers[legacyGroupMember.peer.peerId]!
case let .account(peer):
return peer.peers[peer.peerId]!
}
}
@ -40,6 +45,8 @@ enum PeerInfoMember: Equatable {
return channelMember.presences[channelMember.peer.id] as? TelegramUserPresence
case let .legacyGroupMember(legacyGroupMember):
return legacyGroupMember.presence
case .account:
return nil
}
}
@ -58,6 +65,8 @@ enum PeerInfoMember: Equatable {
}
case let .legacyGroupMember(legacyGroupMember):
return legacyGroupMember.role
case .account:
return .member
}
}
@ -72,6 +81,8 @@ enum PeerInfoMember: Equatable {
}
case .legacyGroupMember:
return nil
case .account:
return nil
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let placeholder: String
let textUpdated: (String) -> Void
let maxLength: Int?
init(
id: AnyHashable,
text: String,
placeholder: String,
textUpdated: @escaping (String) -> Void,
maxLength: Int?
) {
self.id = id
self.text = text
self.placeholder = placeholder
self.textUpdated = textUpdated
self.maxLength = maxLength
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenMultilineInputItemNode()
}
}
private final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode {
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenMultilineInputItem?
private var itemNode: ItemListMultilineInputItemNode?
override init() {
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
self.clipsToBounds = true
self.addSubnode(self.bottomSeparatorNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenMultilineInputItem else {
return 10.0
}
self.item = item
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let inputItem = ItemListMultilineInputItem(presentationData: ItemListPresentationData(presentationData), text: item.text, placeholder: item.placeholder, maxLength: item.maxLength.flatMap { ItemListMultilineInputItemTextLimit(value: $0, display: true) }, sectionId: 0, style: .blocks, textUpdated: { updatedText in
item.textUpdated(updatedText)
}, noInsets: true)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListMultilineInputItemNode
if let current = self.itemNode {
itemNode = current
inputItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
inputItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! ItemListMultilineInputItemNode
self.itemNode = itemNode
self.addSubnode(itemNode)
}
let height = itemNode.contentSize.height
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
var separatorInset: CGFloat = sideInset
if bottomItem != nil {
separatorInset += 49.0
}
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}

View File

@ -433,6 +433,8 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: {
}, greetingStickerNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1183,6 +1183,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: {
}, greetingStickerNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -22,7 +22,7 @@ public final class TelegramRootController: NavigationController {
public var contactsController: ContactsController?
public var callListController: CallListController?
public var chatListController: ChatListController?
public var accountSettingsController: ViewController?
public var accountSettingsController: PeerInfoScreen?
private var permissionsDisposable: Disposable?
private var presentationDataDisposable: Disposable?
@ -110,7 +110,7 @@ public final class TelegramRootController: NavigationController {
sharedContext.switchingData = (nil, nil, nil)
}
let accountSettingsController = restoreSettignsController ?? settingsController(context: self.context, accountManager: context.sharedContext.accountManager, enableDebugActions: !GlobalExperimentalSettings.isAppStoreBuild)
let accountSettingsController = PeerInfoScreen(context: self.context, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, callMessages: [], isSettings: true)
controllers.append(accountSettingsController)
tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : (controllers.count - 2))