diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3f39afd5d3..188bf740b0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6706,6 +6706,8 @@ Sorry for the inconvenience."; "Activity.ChoosingSticker" = "choosing sticker"; "DialogList.SingleChoosingStickerSuffix" = "%@ is choosing sticker"; +"Activity.TappingInteractiveEmoji" = "tapping on %@"; + "WallpaperPreview.Animate" = "Animate"; "WallpaperPreview.AnimateDescription" = "Colors will move when you send messages"; @@ -6866,3 +6868,54 @@ Ads should no longer be synonymous with abuse of user privacy. Let us redefine h "MediaPicker.JpegConversionText" = "Do you want to convert photos to JPEG?"; "MediaPicker.KeepHeic" = "Keep HEIC"; "MediaPicker.ConvertToJpeg" = "Convert to JPEG"; + +"GroupInfo.MemberRequests" = "Member Requests"; + +"InviteLink.Create.RequestApproval" = "Request Admin Approval"; +"InviteLink.Create.RequestApprovalOffInfoGroup" = "New users will be able to join the group without being approved by the admins."; +"InviteLink.Create.RequestApprovalOffInfoChannel" = "New users will be able to join the channel without being approved by the admins."; +"InviteLink.Create.RequestApprovalOnInfoGroup" = "New users will be able to join the group only after having been approved by the admins."; +"InviteLink.Create.RequestApprovalOnInfoChannel" = "New users will be able to join the channel only after having been approved by the admins."; + +"MemberRequests.Title" = "Member Requests"; +"MemberRequests.DescriptionGroup" = "Some [additional links]() are set up to accept requests to join the group."; +"MemberRequests.DescriptionChannel" = "Some [additional links]() are set up to accept requests to join the channel."; + +"MemberRequests.PeopleRequested_1" = "%@ requested to join"; +"MemberRequests.PeopleRequested_2" = "%@ requested to join"; +"MemberRequests.PeopleRequested_3_10" = "%@ requested to join"; +"MemberRequests.PeopleRequested_many" = "%@ requested to join"; +"MemberRequests.PeopleRequested_any" = "%@ requested to join"; + +"MemberRequests.PeopleRequestedShort_1" = "%@ requested"; +"MemberRequests.PeopleRequestedShort_2" = "%@ requested"; +"MemberRequests.PeopleRequestedShort_3_10" = "%@ requested"; +"MemberRequests.PeopleRequestedShort_many" = "%@ requested"; +"MemberRequests.PeopleRequestedShort_any" = "%@ requested"; + +"MemberRequests.AddToGroup" = "Add to Group"; +"MemberRequests.AddToChannel" = "Add to Channel"; +"MemberRequests.Dismiss" = "Dismiss"; + +"MemberRequests.UserAddedToGroup" = "%@ has been added to the group."; +"MemberRequests.UserAddedToChannel" = "%@ has been added to the channel."; + +"MemberRequests.NoRequests" = "No Member Requests"; +"MemberRequests.NoRequestsDescriptionGroup" = "You have no pending requests to join the group."; +"MemberRequests.NoRequestsDescriptionChannel" = "You have no pending requests to join the channel."; + +"Conversation.RequestsToJoin_1" = "%@ Request to Join"; +"Conversation.RequestsToJoin_2" = "%@ Requests to Join"; +"Conversation.RequestsToJoin_3_10" = "%@ Requests to Join"; +"Conversation.RequestsToJoin_many" = "%@ Requests to Join"; +"Conversation.RequestsToJoin_any" = "%@ Requests to Join"; + +"MemberRequests.RequestToJoinGroup" = "Request to Join Group"; +"MemberRequests.RequestToJoinChannel" = "Request to Join Channel"; + +"MemberRequests.RequestToJoinDescriptionGroup" = "This group accepts new members only after they are approved by its admins."; +"MemberRequests.RequestToJoinDescriptionChannel" = "This channel accepts new subscribers only after they are approved by its admins."; + +"MemberRequests.RequestToJoinSent" = "Request to join Sent"; +"MemberRequests.RequestToJoinSentDescriptionGroup" = "You will be added to the group once it admins approve your request."; +"MemberRequests.RequestToJoinSentDescriptionChannel" = "You will be added to the channel once it admins approve your request."; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index abeac311c6..5de2f3f560 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1733,7 +1733,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var animateInputActivitiesFrame = false let inputActivities = inputActivities?.filter({ switch $0.1 { - case .speakingInGroupCall, .interactingWithEmoji, .seeingEmojiInteraction: + case .speakingInGroupCall, .seeingEmojiInteraction: return false default: return true diff --git a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift index 058431d540..4b47e136c8 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift @@ -60,7 +60,9 @@ final class ChatListInputActivitiesNode: ASDisplayNode { text = strings.DialogList_Typing case .choosingSticker: text = strings.Activity_ChoosingSticker - case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji: + case let .interactingWithEmoji(emoticon, _, _): + text = strings.Activity_TappingInteractiveEmoji(emoticon).string + case .speakingInGroupCall, .seeingEmojiInteraction: text = "" } let string = NSAttributedString(string: text, font: textFont, textColor: color) @@ -80,7 +82,9 @@ final class ChatListInputActivitiesNode: ASDisplayNode { state = .typingText(string, lightColor) case .choosingSticker: state = .choosingSticker(string, lightColor) - case .seeingEmojiInteraction, .interactingWithEmoji: + case .interactingWithEmoji: + state = .interactingWithEmoji(string, lightColor) + case .seeingEmojiInteraction: state = .none } } else { diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift index 7bef4a850c..1944d18b29 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift @@ -128,7 +128,7 @@ public class ChatTitleActivityContentNode: ASDisplayNode { if case .center = alignment { self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0 + offset) } else { - self.textNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + offset) + self.textNode.position = CGPoint(x: size.width / 2.0 + 3.0, y: size.height / 2.0 + offset) } return size } diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift index a2a1abf891..b9013f66ff 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift @@ -24,6 +24,7 @@ public enum ChatTitleActivityNodeState: Equatable { case recordingVideo(NSAttributedString, UIColor) case playingGame(NSAttributedString, UIColor) case choosingSticker(NSAttributedString, UIColor) + case interactingWithEmoji(NSAttributedString, UIColor) func contentNode() -> ChatTitleActivityContentNode? { switch self { @@ -43,6 +44,8 @@ public enum ChatTitleActivityNodeState: Equatable { return ChatPlayingActivityContentNode(text: text, color: color) case let .choosingSticker(text, color): return ChatChoosingStickerActivityContentNode(text: text, color: color) + case let .interactingWithEmoji(text, _): + return ChatTitleActivityContentNode(text: text) } } diff --git a/submodules/GraphUI/Sources/ChartStackSection.swift b/submodules/GraphUI/Sources/ChartStackSection.swift index 5736331e9f..dd1e60dd6a 100644 --- a/submodules/GraphUI/Sources/ChartStackSection.swift +++ b/submodules/GraphUI/Sources/ChartStackSection.swift @@ -16,10 +16,13 @@ private enum Constants { private class LeftAlignedIconButton: UIButton { override func titleRect(forContentRect contentRect: CGRect) -> CGRect { - let titleRect = super.titleRect(forContentRect: contentRect) + var titleRect = super.titleRect(forContentRect: contentRect) let imageSize = currentImage?.size ?? .zero - let availableWidth = contentRect.width - imageEdgeInsets.right - imageSize.width - titleRect.width - return titleRect.offsetBy(dx: round(availableWidth / 2) - imageEdgeInsets.left, dy: 0) + titleRect.origin.x = imageSize.width + 2.0 +// +// let availableWidth = contentRect.width - imageEdgeInsets.right - imageSize.width - titleRect.width +// return titleRect.offsetBy(dx: round(availableWidth / 2) - imageEdgeInsets.left, dy: 0) + return titleRect } } diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index e43bd900f0..85fa60baae 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -53,6 +53,8 @@ swift_library( "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/AvatarNode:AvatarNode", + "//submodules/LocalizedPeerData:LocalizedPeerData", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 6e2e6e80f4..bd5bc8f6fc 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -34,6 +34,7 @@ private final class InviteLinkEditControllerArguments { private enum InviteLinksEditSection: Int32 { case time case usage + case requestApproval case revoke } @@ -65,10 +66,15 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool) case usageInfo(PresentationTheme, String) + case requestApproval(PresentationTheme, String, Bool) + case requestApprovalInfo(PresentationTheme, String) + case revoke(PresentationTheme, String) var section: ItemListSectionId { switch self { + case .requestApproval, .requestApprovalInfo: + return InviteLinksEditSection.requestApproval.rawValue case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo: return InviteLinksEditSection.time.rawValue case .usageHeader, .usagePicker, .usageCustomPicker, .usageInfo: @@ -80,26 +86,30 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { var stableId: Int32 { switch self { - case .timeHeader: + case .requestApproval: return 0 - case .timePicker: + case .requestApprovalInfo: return 1 - case .timeExpiryDate: + case .timeHeader: return 2 - case .timeCustomPicker: + case .timePicker: return 3 - case .timeInfo: + case .timeExpiryDate: return 4 - case .usageHeader: + case .timeCustomPicker: return 5 - case .usagePicker: + case .timeInfo: return 6 - case .usageCustomPicker: + case .usageHeader: return 7 - case .usageInfo: + case .usagePicker: return 8 - case .revoke: + case .usageCustomPicker: return 9 + case .usageInfo: + return 10 + case .revoke: + return 11 } } @@ -159,6 +169,18 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } + case let .requestApproval(lhsTheme, lhsText, lhsValue): + if case let .requestApproval(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .requestApprovalInfo(lhsTheme, lhsText): + if case let .requestApprovalInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .revoke(lhsTheme, lhsText): if case let .revoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -266,6 +288,16 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { }) case let .usageInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .requestApproval(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.requestApproval = value + return updatedState + } + }) + case let .requestApprovalInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .revoke(_, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.revoke() @@ -277,8 +309,16 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, presentationData: PresentationData) -> [InviteLinksEditEntry] { var entries: [InviteLinksEditEntry] = [] - entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased())) + entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval)) + var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + if state.requestApproval { + requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + } else { + requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + } + entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText)) + entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased())) entries.append(.timePicker(presentationData.theme, state.time)) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -294,16 +334,17 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: } entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo)) - entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) - entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage)) - - var customValue = false - if case .custom = state.usage { - customValue = true + if !state.requestApproval { + entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) + entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage)) + + var customValue = false + if case .custom = state.usage { + customValue = true + } + entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue)) + entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo)) } - entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue)) - - entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo)) if let _ = invite { entries.append(.revoke(presentationData.theme, presentationData.strings.InviteLink_Create_Revoke)) @@ -315,6 +356,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: private struct InviteLinkEditControllerState: Equatable { var usage: InviteLinkUsageLimit var time: InviteLinkTimeLimit + var requestApproval = false var pickingTimeLimit = false var pickingUsageLimit = false var updating = false @@ -343,9 +385,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio timeLimit = .unlimited } - initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: invite.requestApproval, pickingTimeLimit: false, pickingUsageLimit: false) } else { - initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, pickingTimeLimit: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, requestApproval: false, pickingTimeLimit: false, pickingUsageLimit: false) } let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -443,8 +485,10 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } let usageLimit = state.usage.value + let requestNeeded = state.requestApproval + if invite == nil { - let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, expireDate: expireDate, usageLimit: usageLimit) + let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -458,7 +502,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) } else if let invite = invite { - let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit) + let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -476,7 +520,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let previousState = previousState.swap(state) var animateChanges = false - if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit { + if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit || previousState.requestApproval != state.requestApproval { animateChanges = true } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index e8f48f404c..0a4afeebad 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -9,18 +9,22 @@ import PresentationDataUtils import AnimatedStickerNode import TelegramAnimatedStickerNode import AccountContext +import Markdown +import TextFormat class InviteLinkHeaderItem: ListViewItem, ItemListItem { let context: AccountContext let theme: PresentationTheme let text: String let sectionId: ItemListSectionId + let linkAction: ((ItemListTextItemLinkAction) -> Void)? - init(context: AccountContext, theme: PresentationTheme, text: String, sectionId: ItemListSectionId) { + init(context: AccountContext, theme: PresentationTheme, text: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { self.context = context self.theme = theme self.text = text self.sectionId = sectionId + self.linkAction = linkAction } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -82,6 +86,16 @@ class InviteLinkHeaderItemNode: ListViewItemNode { self.addSubnode(self.animationNode) } + override public func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.view.addGestureRecognizer(recognizer) + } + func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -89,7 +103,10 @@ class InviteLinkHeaderItemNode: ListViewItemNode { let leftInset: CGFloat = 32.0 + params.leftInset let topInset: CGFloat = 92.0 - let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor) + let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + })) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) @@ -124,4 +141,27 @@ class InviteLinkHeaderItemNode: ListViewItemNode { override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + let titleFrame = self.titleNode.frame + if let item = self.item, titleFrame.contains(location) { + if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + item.linkAction?(.tap(url)) + } + } + } + default: + break + } + } + default: + break + } + } } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 406b48f794..67a87faaf3 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -283,7 +283,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, let mainInvite: ExportedInvitation? var isPublic = false if let peer = peer, let address = peer.addressName, !address.isEmpty && admin == nil { - mainInvite = ExportedInvitation(link: "t.me/\(address)", isPermanent: true, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil) + mainInvite = ExportedInvitation(link: "t.me/\(address)", isPermanent: true, requestApproval: false, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, approvedDate: nil) isPublic = true } else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) { mainInvite = invite diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift new file mode 100644 index 0000000000..d156597672 --- /dev/null +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -0,0 +1,289 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import OverlayStatusController +import AccountContext +import AlertUI +import PresentationDataUtils +import AppBundle +import ContextUI +import TelegramStringFormatting +import ItemListPeerActionItem +import ItemListPeerItem +import ShareController +import UndoUI + +private final class InviteRequestsControllerArguments { + let context: AccountContext + let openLinks: () -> Void + let openPeer: (EnginePeer) -> Void + let approveRequest: (EnginePeer) -> Void + let denyRequest: (EnginePeer) -> Void + let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?) -> Void + + init(context: AccountContext, openLinks: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, approveRequest: @escaping (EnginePeer) -> Void, denyRequest: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) { + self.context = context + self.openLinks = openLinks + self.openPeer = openPeer + self.approveRequest = approveRequest + self.denyRequest = denyRequest + self.peerContextAction = peerContextAction + } +} + +private enum InviteRequestsSection: Int32 { + case header + case requests +} + +private enum InviteRequestsEntry: ItemListNodeEntry { + case header(PresentationTheme, String) + + case requestsHeader(PresentationTheme, String) + case request(Int32, PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerInvitationImportersState.Importer, Bool) + + var section: ItemListSectionId { + switch self { + case .header: + return InviteRequestsSection.header.rawValue + case .requestsHeader, .request: + return InviteRequestsSection.requests.rawValue + } + } + + var stableId: Int32 { + switch self { + case .header: + return 0 + case .requestsHeader: + return 1 + case let .request(index, _, _, _, _, _): + return 2 + index + } + } + + static func ==(lhs: InviteRequestsEntry, rhs: InviteRequestsEntry) -> Bool { + switch lhs { + case let .header(lhsTheme, lhsText): + if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .requestsHeader(lhsTheme, lhsText): + if case let .requestsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .request(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsImporter, lhsIsGroup): + if case let .request(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsImporter, rhsIsGroup) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsImporter == rhsImporter, lhsIsGroup == rhsIsGroup { + return true + } else { + return false + } + } + } + + static func <(lhs: InviteRequestsEntry, rhs: InviteRequestsEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! InviteRequestsControllerArguments + switch self { + case let .header(theme, text): + return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: text, sectionId: self.section, linkAction: { _ in + arguments.openLinks() + }) + case let .requestsHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .request(_, _, dateTimeFormat, nameDisplayOrder, importer, isGroup): + return ItemListInviteRequestItem(context: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, importer: importer, isGroup: isGroup, sectionId: self.section, style: .blocks, tapAction: { + if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) { + arguments.openPeer(peer) + } + }, addAction: { + if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) { + arguments.approveRequest(peer) + } + }, dismissAction: { + if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) { + arguments.denyRequest(peer) + } + }, contextAction: { node, gesture in + if let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) { + arguments.peerContextAction(peer, node, gesture) + } + }) + } + } +} + +private func inviteRequestsControllerEntries(presentationData: PresentationData, peer: EnginePeer?, importers: [PeerInvitationImportersState.Importer]?, isGroup: Bool) -> [InviteRequestsEntry] { + var entries: [InviteRequestsEntry] = [] + + if let importers = importers, !importers.isEmpty { + let helpText: String + if case let .channel(peer) = peer, case .broadcast = peer.info { + helpText = presentationData.strings.MemberRequests_DescriptionChannel + } else { + helpText = presentationData.strings.MemberRequests_DescriptionGroup + } + entries.append(.header(presentationData.theme, helpText)) + + entries.append(.requestsHeader(presentationData.theme, presentationData.strings.MemberRequests_PeopleRequested(Int32(importers.count)).uppercased())) + + var index: Int32 = 0 + for importer in importers { + entries.append(.request(index, presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, importer, isGroup)) + index += 1 + } + } + + return entries +} + +public func inviteRequestsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, existingContext: PeerInvitationImportersContext? = nil) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? + var navigateToProfileImpl: ((EnginePeer) -> Void)? + + var dismissTooltipsImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let updateDisposable = MetaDisposable() + actionsDisposable.add(updateDisposable) + + var getControllerImpl: (() -> ViewController?)? + + let importersContext = existingContext ?? context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil) + + let arguments = InviteRequestsControllerArguments(context: context, openLinks: { + let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) + pushControllerImpl?(controller) + }, openPeer: { peer in + navigateToProfileImpl?(peer) + }, approveRequest: { peer in + importersContext.update(peer.id, action: .approve) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: presentationData.strings.MemberRequests_UserAddedToChannel(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, denyRequest: { peer in + importersContext.update(peer.id, action: .deny) + }, peerContextAction: { peer, node, gesture in + guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?() else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + dismissTooltipsImpl?() + + }))) + + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + presentInGlobalOverlayImpl?(contextController) + }) + + let previousEntries = Atomic<[InviteRequestsEntry]>(value: []) + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ), + importersContext.state + ) + |> map { presentationData, peer, importersState -> (ItemListControllerState, (ItemListNodeState, Any)) in + var isGroup = true + if case let .channel(channel) = peer, case .broadcast = channel.info { + isGroup = false + } + + var emptyStateItem: ItemListControllerEmptyStateItem? + if importersState.hasLoadedOnce && importersState.importers.isEmpty { + emptyStateItem = InviteRequestsEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, isGroup: isGroup) + } + + let entries = inviteRequestsControllerEntries(presentationData: presentationData, peer: peer, importers: importersState.hasLoadedOnce ? importersState.importers : nil, isGroup: isGroup) + let previousEntries = previousEntries.swap(entries) + + let crossfade = !previousEntries.isEmpty && entries.isEmpty + let animateChanges = (!previousEntries.isEmpty && !entries.isEmpty) && previousEntries.count != entries.count + + let title: ItemListControllerTitle = .text(presentationData.strings.MemberRequests_Title) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfade, animateChanges: animateChanges) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.willDisappear = { _ in + dismissTooltipsImpl?() + } + controller.didDisappear = { [weak controller] _ in + controller?.clearItemNodesHighlight(animated: true) + } + controller.visibleBottomContentOffsetChanged = { offset in + if case let .known(value) = offset, value < 40.0 { + + } + } + pushControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) + } + } + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window(.root), with: p) + } + } + presentInGlobalOverlayImpl = { [weak controller] c in + if let controller = controller { + controller.presentInGlobalOverlay(c) + } + } + navigateToProfileImpl = { [weak controller] peer in + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false) { + navigationController.pushViewController(controller) + } + } + getControllerImpl = { [weak controller] in + return controller + } + dismissTooltipsImpl = { [weak controller] in + controller?.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + controller?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + } + return controller +} diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift b/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift new file mode 100644 index 0000000000..6956a7bfc1 --- /dev/null +++ b/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift @@ -0,0 +1,110 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import AccountContext + +final class InviteRequestsEmptyStateItem: ItemListControllerEmptyStateItem { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let isGroup: Bool + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, isGroup: Bool) { + self.context = context + self.theme = theme + self.strings = strings + self.isGroup = isGroup + } + + func isEqual(to: ItemListControllerEmptyStateItem) -> Bool { + if let item = to as? InviteRequestsEmptyStateItem { + return self.theme === item.theme && self.strings === item.strings && self.isGroup == item.isGroup + } else { + return false + } + } + + func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode { + if let current = current as? InviteRequestsEmptyStateItemNode { + current.item = self + return current + } else { + return InviteRequestsEmptyStateItemNode(item: self) + } + } +} + +final class InviteRequestsEmptyStateItemNode: ItemListControllerEmptyStateItemNode { + private var animationNode: AnimatedStickerNode + private let titleNode: ASTextNode + private let textNode: ASTextNode + private var validLayout: (ContainerViewLayout, CGFloat)? + + var item: InviteRequestsEmptyStateItem { + didSet { + self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings) + if let (layout, navigationHeight) = self.validLayout { + self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate) + } + } + } + + init(item: InviteRequestsEmptyStateItem) { + self.item = item + + self.animationNode = AnimatedStickerNode() + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Invite"), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + self.animationNode.visibility = true + + self.titleNode = ASTextNode() + self.titleNode.isUserInteractionEnabled = false + + self.textNode = ASTextNode() + self.textNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.animationNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings) + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.titleNode.attributedText = NSAttributedString(string: strings.MemberRequests_NoRequests, font: Font.bold(17.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.item.isGroup ? strings.MemberRequests_NoRequestsDescriptionGroup : strings.MemberRequests_NoRequestsDescriptionChannel, font: Font.regular(14.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center) + } + + override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight) + var insets = layout.insets(options: []) + insets.top += navigationBarHeight + + let imageSpacing: CGFloat = 20.0 + let textSpacing: CGFloat = 8.0 + + let imageSize = CGSize(width: 96.0, height: 96.0) + let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0 + + self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize) + self.animationNode.updateLayout(size: imageSize) + + let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom))) + let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom))) + + let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height + let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0) + + transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0) + transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize)) + } +} diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index 18042da0f4..dbcd124664 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -344,7 +344,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { if let invite = item.invite { let count = invite.count ?? 0 if count > 0 { - subtitleText = item.presentationData.strings.InviteLink_PeopleJoinedShort(count) + if invite.requestApproval { + subtitleText = item.presentationData.strings.MemberRequests_PeopleRequestedShort(count) + } else { + subtitleText = item.presentationData.strings.InviteLink_PeopleJoinedShort(count) + } } else { if let usageLimit = invite.usageLimit, count == 0 && !availability.isZero { subtitleText = item.presentationData.strings.InviteLink_PeopleCanJoin(usageLimit) diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift new file mode 100644 index 0000000000..961a9fdc72 --- /dev/null +++ b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift @@ -0,0 +1,559 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import ItemListUI +import ShimmerEffect +import LocalizedPeerData +import AvatarNode +import AccountContext +import SolidRoundedButtonNode + +public class ItemListInviteRequestItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let importer: PeerInvitationImportersState.Importer? + let isGroup: Bool + public let sectionId: ItemListSectionId + let style: ItemListStyle + let tapAction: (() -> Void)? + let addAction: (() -> Void)? + let dismissAction: (() -> Void)? + let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + public let tag: ItemListItemTag? + + public init( + context: AccountContext, + presentationData: ItemListPresentationData, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + importer: PeerInvitationImportersState.Importer?, + isGroup: Bool, + sectionId: ItemListSectionId, + style: ItemListStyle, + tapAction: (() -> Void)?, + addAction: (() -> Void)?, + dismissAction: (() -> Void)?, + contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, + tag: ItemListItemTag? = nil + ) { + self.context = context + self.presentationData = presentationData + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.importer = importer + self.isGroup = isGroup + self.sectionId = sectionId + self.style = style + self.tapAction = tapAction + self.addAction = addAction + self.dismissAction = dismissAction + self.contextAction = contextAction + self.tag = tag + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } + if nextItem == nil { + last = true + } + } + let node = ItemListInviteRequestItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListInviteRequestItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } + if nextItem == nil { + last = true + } + } + + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + self.tapAction?() + } +} + +private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)) + +public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let extractedBackgroundImageNode: ASImageNode + + private let containerNode: ContextControllerSourceNode + private let contextSourceNode: ContextExtractedContentContainingNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let offsetContainerNode: ASDisplayNode + + fileprivate let avatarNode: AvatarNode + private let titleNode: TextNode + private let subtitleNode: TextNode + private let dateNode: TextNode + private let addButton: SolidRoundedButtonNode + private let dismissButton: HighlightableButtonNode + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + + private var layoutParams: (ItemListInviteRequestItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? + + public var tag: ItemListItemTag? + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + self.maskNode = ASImageNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.offsetContainerNode = ASDisplayNode() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.contentMode = .left + self.subtitleNode.contentsScale = UIScreen.main.scale + + self.dateNode = TextNode() + self.dateNode.isUserInteractionEnabled = false + self.dateNode.contentMode = .left + self.dateNode.contentsScale = UIScreen.main.scale + + self.addButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + self.dismissButton = HighlightableButtonNode() + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.avatarNode = AvatarNode(font: avatarFont) + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.isAccessibilityElement = true + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) + + self.offsetContainerNode.addSubnode(self.avatarNode) + self.offsetContainerNode.addSubnode(self.titleNode) + self.offsetContainerNode.addSubnode(self.subtitleNode) + self.offsetContainerNode.addSubnode(self.dateNode) + self.offsetContainerNode.addSubnode(self.addButton) + self.offsetContainerNode.addSubnode(self.dismissButton) + + self.addButton.pressed = { [weak self] in + if let (item, _, _, _, _) = self?.layoutParams { + item.addAction?() + } + } + self.dismissButton.addTarget(self, action: #selector(self.dismissPressed), forControlEvents: .touchUpInside) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let _ = item.importer, let contextAction = item.contextAction else { + gesture.cancel() + return + } + contextAction(strongSelf.contextSourceNode, gesture) + } + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.plainBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } + } + + public func asyncLayout() -> (_ item: ItemListInviteRequestItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeDateLayout = TextNode.asyncLayout(self.dateNode) + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors, firstWithHeader, last in + var updatedTheme: PresentationTheme? + + let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + var titleText: String + var subtitleText: String + var dateText: String + + if let importer = item.importer, let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) { + titleText = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder) + if case .user = peer { + subtitleText = " " + } else { + subtitleText = "" + } + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: importer.date, relativeTo: timestamp, dateTimeFormat: item.dateTimeFormat) + } else { + titleText = " " + subtitleText = " " + dateText = " " + } + + let titleAttributedString = NSAttributedString(string: titleText, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let subtitleAttributedString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + let dateAttributedString = NSAttributedString(string: dateText, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + + let leftInset: CGFloat = 62.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + let verticalInset: CGFloat = 9.0 + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = 1.0 + + let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0 + let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + 24.0 + + var insets: UIEdgeInsets + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + insets.top = firstWithHeader ? 29.0 : 0.0 + insets.bottom = 0.0 + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors) + } + + let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight)) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.layoutParams = (item, params, neighbors, firstWithHeader, last) + + strongSelf.accessibilityLabel = titleAttributedString.string + strongSelf.accessibilityValue = subtitleAttributedString.string + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.isGestureEnabled = item.contextAction != nil + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) + let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + strongSelf.extractedRect = extractedRect + strongSelf.nonExtractedRect = nonExtractedRect + + if strongSelf.contextSourceNode.isExtractedToContextPreview { + strongSelf.extractedBackgroundImageNode.frame = extractedRect + } else { + strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect + } + strongSelf.contextSourceNode.contentRect = extractedRect + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + let transition = ContainedViewLayoutTransition.immediate + + let _ = titleApply() + let _ = subtitleApply() + let _ = dateApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + + let stripeInset: CGFloat + if case .none = neighbors.bottom { + stripeInset = 0.0 + } else { + stripeInset = leftInset + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: stripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - stripeInset, height: separatorHeight)) + strongSelf.bottomStripeNode.isHidden = last + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + let avatarSize: CGSize = CGSize(width: 40.0, height: 40.0) + let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 9.0, y: verticalInset + 2.0), size: avatarSize) + strongSelf.avatarNode.frame = avatarFrame + + if let importer = item.importer, let peer = importer.peer.peer.flatMap({ EnginePeer($0) }) { + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false) + } + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - rightInset - dateLayout.size.width, y: verticalInset + 2.0), size: dateLayout.size)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + + strongSelf.addButton.title = item.isGroup ? item.presentationData.strings.MemberRequests_AddToGroup : item.presentationData.strings.MemberRequests_AddToChannel + if let _ = updatedTheme { + strongSelf.addButton.updateTheme(SolidRoundedButtonTheme(theme: item.presentationData.theme)) + } + strongSelf.dismissButton.setTitle(item.presentationData.strings.MemberRequests_Dismiss, with: Font.bold(15.0), with: item.presentationData.theme.list.itemAccentColor, for: .normal) + + let addHeight = strongSelf.addButton.updateLayout(width: 138.0, transition: .immediate) + strongSelf.addButton.frame = CGRect(x: leftInset, y: verticalInset + titleLayout.size.height + 7.0, width: 138.0, height: addHeight) + + let dismissSize = strongSelf.dismissButton.measure(layout.size) + strongSelf.dismissButton.frame = CGRect(origin: CGPoint(x: leftInset + 138.0 + 24.0, y: verticalInset + titleLayout.size.height + 14.0), size: dismissSize) + + if item.importer == nil { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + strongSelf.addSubnode(shimmerNode) + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = 180.0 + let subtitleLineWidth: CGFloat = 60.0 + let lineDiameter: CGFloat = 10.0 + + let iconFrame = strongSelf.avatarNode.frame + shapes.append(.circle(iconFrame)) + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + let subtitleFrame = strongSelf.subtitleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter)) + + shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize) + } else if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } + } + }) + } + } + + @objc private func dismissPressed() { + if let (item, _, _, _, _) = self.layoutParams { + item.dismissAction?() + } + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } +} diff --git a/submodules/JoinLinkPreviewUI/BUILD b/submodules/JoinLinkPreviewUI/BUILD index 84d706a26e..2d87887404 100644 --- a/submodules/JoinLinkPreviewUI/BUILD +++ b/submodules/JoinLinkPreviewUI/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/SelectablePeerNode:SelectablePeerNode", "//submodules/PeerInfoUI:PeerInfoUI", "//submodules/UndoUI:UndoUI", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", ], visibility = [ "//visibility:public", diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index f77e1e9bc9..47f5121229 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -20,6 +20,8 @@ public final class JoinLinkPreviewController: ViewController { private let context: AccountContext private let link: String + private var isRequest = false + private var isGroup = false private let navigateToPeer: (EnginePeer.Id, ChatPeekTimeout?) -> Void private let parentNavigationController: NavigationController? private var resolvedState: ExternalJoiningChatState? @@ -76,9 +78,15 @@ public final class JoinLinkPreviewController: ViewController { if let strongSelf = self { strongSelf.resolvedState = result switch result { - case let .invite(title, photoRepresentation, participantsCount, participants): - let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false) - strongSelf.controllerNode.setPeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants?.map({ EnginePeer($0) }) ?? [], data: data) + case let .invite(flags, title, about, photoRepresentation, participantsCount, participants): + if flags.requestNeeded { + strongSelf.isRequest = true + strongSelf.isGroup = !flags.isBroadcast + strongSelf.controllerNode.setRequestPeer(image: photoRepresentation, title: title, about: about, memberCount: participantsCount, isGroup: !flags.isBroadcast) + } else { + let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false) + strongSelf.controllerNode.setInvitePeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants?.map({ EnginePeer($0) }) ?? [], data: data) + } case let .alreadyJoined(peerId): strongSelf.navigateToPeer(peerId, nil) strongSelf.dismiss() @@ -121,10 +129,14 @@ public final class JoinLinkPreviewController: ViewController { private func join() { self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] peerId in if let strongSelf = self { - if let peerId = peerId { - strongSelf.navigateToPeer(peerId, nil) - strongSelf.dismiss() + if strongSelf.isRequest { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: strongSelf.isGroup ? strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } else { + if let peerId = peerId { + strongSelf.navigateToPeer(peerId, nil) + } } + strongSelf.dismiss() } }, error: { [weak self] error in if let strongSelf = self { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index eb330830ad..a2e544bceb 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -8,6 +8,27 @@ import TelegramPresentationData import AccountContext import ShareController +private func closeButtonImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + struct JoinLinkPreviewData { let isGroup: Bool let isJoined: Bool @@ -24,18 +45,17 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi private let dimNode: ASDisplayNode private let wrappingScrollNode: ASScrollNode - private let cancelButtonNode: ASButtonNode private let contentContainerNode: ASDisplayNode - private let contentBackgroundNode: ASImageNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentBackgroundNode: ASDisplayNode private var contentNode: (ASDisplayNode & ShareContentContainerNode)? private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? private var animateContentNodeOffsetFromBackgroundOffset: CGFloat? - private let actionsBackgroundNode: ASImageNode - private let actionButtonNode: ShareActionButtonNode - private let actionSeparatorNode: ASDisplayNode + private let cancelButton: HighlightableButtonNode var dismiss: (() -> Void)? var cancel: (() -> Void)? @@ -51,28 +71,12 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi init(context: AccountContext, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { self.context = context - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData self.requestLayout = requestLayout - let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) - let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) - - let theme = self.presentationData.theme - let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) - })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) - - let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) - })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) - self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false @@ -81,35 +85,22 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.cancelButtonNode = ASButtonNode() - self.cancelButtonNode.displaysAsynchronously = false - self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) - self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) - self.contentContainerNode = ASDisplayNode() self.contentContainerNode.isOpaque = false - self.contentContainerNode.clipsToBounds = true - self.contentBackgroundNode = ASImageNode() - self.contentBackgroundNode.displaysAsynchronously = false - self.contentBackgroundNode.displayWithoutProcessing = true - self.contentBackgroundNode.image = roundedBackground + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.cornerRadius = 16.0 - self.actionsBackgroundNode = ASImageNode() - self.actionsBackgroundNode.isLayerBacked = true - self.actionsBackgroundNode.displayWithoutProcessing = true - self.actionsBackgroundNode.displaysAsynchronously = false - self.actionsBackgroundNode.image = halfRoundedBackground + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)) + }) - self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) - self.actionButtonNode.displaysAsynchronously = false - self.actionButtonNode.titleNode.displaysAsynchronously = false - self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) - - self.actionSeparatorNode = ASDisplayNode() - self.actionSeparatorNode.isLayerBacked = true - self.actionSeparatorNode.displaysAsynchronously = false - self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.contentBackgroundNode = ASDisplayNode() + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + + self.cancelButton = HighlightableButtonNode() + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) super.init() @@ -121,27 +112,20 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) + + self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + + - self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) - - self.wrappingScrollNode.addSubnode(self.cancelButtonNode) - self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - - self.actionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside) - - self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + self.backgroundNode.addSubnode(self.effectNode) + self.backgroundNode.addSubnode(self.contentBackgroundNode) + self.wrappingScrollNode.addSubnode(self.backgroundNode) self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.contentContainerNode.addSubnode(self.actionSeparatorNode) - self.contentContainerNode.addSubnode(self.actionsBackgroundNode) - self.contentContainerNode.addSubnode(self.actionButtonNode) - - self.transitionToContentNode(ShareLoadingContainerNode(theme: theme, forceNativeAppearance: false)) - - self.actionButtonNode.alpha = 0.0 - self.actionSeparatorNode.alpha = 0.0 - self.actionsBackgroundNode.alpha = 0.0 + self.wrappingScrollNode.addSubnode(self.cancelButton) + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false)) + self.ready.set(.single(true)) self.didSetReady = true } @@ -230,45 +214,31 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi insets.top = max(10.0, insets.top) var bottomInset: CGFloat = 10.0 + cleanInsets.bottom - if insets.bottom > 0 { + if insets.bottom > 0.0 { bottomInset -= 12.0 } - let buttonHeight: CGFloat = 57.0 - let sectionSpacing: CGFloat = 8.0 - let titleAreaHeight: CGFloat = 64.0 - let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset, insets.bottom) - let width = min(layout.size.width, layout.size.height) - 20.0 + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) let sideInset = floor((layout.size.width - width) / 2.0) let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) - let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0) - - let bottomGridInset = buttonHeight - - self.containerLayout = (layout, navigationBarHeight, bottomGridInset) + let contentFrame = contentContainerFrame + + self.containerLayout = (layout, navigationBarHeight, 0.0) self.scheduledLayoutTransitionRequest = nil transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) - + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset))) - - transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) - - transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) - - let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height)) if let contentNode = self.contentNode { - transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) - contentNode.updateLayout(size: gridSize, bottomInset: bottomGridInset, transition: transition) + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: 0.0), size: gridSize)) + contentNode.updateLayout(size: gridSize, bottomInset: 0.0, transition: transition) } } @@ -282,14 +252,11 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi if insets.bottom > 0 { bottomInset -= 12.0 } - let buttonHeight: CGFloat = 57.0 - let sectionSpacing: CGFloat = 8.0 - - let width = min(layout.size.width, layout.size.height) - 20.0 + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) let sideInset = floor((layout.size.width - width) / 2.0) - let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing + let maximumContentHeight = layout.size.height - insets.top - max(bottomInset, insets.bottom) let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - contentOffset), size: contentFrame.size) @@ -299,11 +266,15 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi if backgroundFrame.maxY > contentFrame.maxY { backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY } - if backgroundFrame.size.height < buttonHeight + 32.0 { - backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height - backgroundFrame.size.height = buttonHeight + 32.0 - } - transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + backgroundFrame.size.height += 2000.0 + + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + let cancelSize = CGSize(width: 44.0, height: 44.0) + let cancelFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - cancelSize.width - 3.0, y: backgroundFrame.minY + 6.0), size: cancelSize) + transition.updateFrame(node: self.cancelButton, frame: cancelFrame) if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset { self.animateContentNodeOffsetFromBackgroundOffset = nil @@ -328,10 +299,6 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi self.cancel?() } - @objc func installActionButtonPressed() { - self.join?() - } - func animateIn() { if self.contentNode != nil { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) @@ -382,11 +349,8 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) { - return result - } if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButton.bounds.contains(self.convert(point, to: self.cancelButton)) { return self.dimNode.view } } @@ -430,12 +394,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi self.setNeedsLayout() } - func transitionToProgress(signal: Signal) { - let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) - transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) - transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) - transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) - + func transitionToProgress(signal: Signal) { self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false), fastOut: true) let timestamp = CACurrentMediaTime() self.disposable.set(signal.start(completed: { [weak self] in @@ -449,19 +408,19 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi })) } - func setPeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { - let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut) - transition.updateAlpha(node: self.actionButtonNode, alpha: 1.0) - transition.updateAlpha(node: self.actionSeparatorNode, alpha: 1.0) - transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 1.0) - - self.actionButtonNode.isEnabled = true - if data.isJoined { - self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_LinkDialogOpen, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) - } else { - self.actionButtonNode.setTitle(data.isGroup ? self.presentationData.strings.Invitation_JoinGroup : self.presentationData.strings.Channel_JoinChannel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { + let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, memberCount: memberCount, members: members)) + contentNode.join = { [weak self] in + self?.join?() } - - self.transitionToContentNode(JoinLinkPreviewPeerContentNode(context: self.context, image: image, title: title, memberCount: memberCount, members: members, isGroup: data.isGroup, theme: self.presentationData.theme, strings: self.presentationData.strings)) + self.transitionToContentNode(contentNode) + } + + func setRequestPeer(image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, isGroup: Bool) { + let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .request(isGroup: isGroup, image: image, title: title, about: about, memberCount: memberCount)) + contentNode.join = { [weak self] in + self?.join?() + } + self.transitionToContentNode(contentNode) } } diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index d14c5cb6f0..b9c074909d 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -8,8 +8,9 @@ import AvatarNode import AccountContext import SelectablePeerNode import ShareController +import SolidRoundedButtonNode -private let avatarFont = avatarPlaceholderFont(size: 26.0) +private let avatarFont = avatarPlaceholderFont(size: 42.0) private final class MoreNode: ASDisplayNode { private let avatarNode = AvatarNode(font: Font.regular(24.0)) @@ -27,61 +28,116 @@ private final class MoreNode: ASDisplayNode { } final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { + enum Content { + case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer]) + case request(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32) + + var isGroup: Bool { + switch self { + case let .invite(isGroup, _, _, _, _), let .request(isGroup, _, _, _, _): + return isGroup + } + } + + var image: TelegramMediaImageRepresentation? { + switch self { + case let .invite(_, image, _, _, _), let .request(_, image, _, _, _): + return image + } + } + + var title: String { + switch self { + case let .invite(_, _, title, _, _), let .request(_, _, title, _, _): + return title + } + } + + var memberCount: Int32 { + switch self { + case let .invite(_, _, _, memberCount, _), let .request(_, _, _, _, memberCount): + return memberCount + } + } + } + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private let avatarNode: AvatarNode private let titleNode: ASTextNode private let countNode: ASTextNode + private let aboutNode: ASTextNode + private let descriptionNode: ASTextNode private let peersScrollNode: ASScrollNode private let peerNodes: [SelectablePeerNode] private let moreNode: MoreNode? - init(context: AccountContext, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], isGroup: Bool, theme: PresentationTheme, strings: PresentationStrings) { + private let actionButtonNode: SolidRoundedButtonNode + + var join: (() -> Void)? + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: JoinLinkPreviewPeerContentNode.Content) { self.avatarNode = AvatarNode(font: avatarFont) self.titleNode = ASTextNode() self.countNode = ASTextNode() + self.aboutNode = ASTextNode() + self.aboutNode.maximumNumberOfLines = 6 + self.descriptionNode = ASTextNode() + self.descriptionNode.maximumNumberOfLines = 0 + self.descriptionNode.textAlignment = .center self.peersScrollNode = ASScrollNode() self.peersScrollNode.view.showsHorizontalScrollIndicator = false + self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0, gloss: false) + let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor) - self.peerNodes = members.map { peer in - let node = SelectablePeerNode() - node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), synchronousLoad: false) - node.theme = itemTheme - return node - } - - if members.count < Int(memberCount) { - self.moreNode = MoreNode(count: Int(memberCount) - members.count) + if case let .invite(isGroup, _, _, memberCount, members) = content { + self.peerNodes = members.map { peer in + let node = SelectablePeerNode() + node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), synchronousLoad: false) + node.theme = itemTheme + return node + } + + if members.count < Int(memberCount) { + self.moreNode = MoreNode(count: Int(memberCount) - members.count) + } else { + self.moreNode = nil + } + + self.actionButtonNode.title = isGroup ? strings.Invitation_JoinGroup : strings.Channel_JoinChannel } else { + self.peerNodes = [] self.moreNode = nil + + self.actionButtonNode.title = content.isGroup ? strings.MemberRequests_RequestToJoinGroup : strings.MemberRequests_RequestToJoinChannel } super.init() - let peer = TelegramGroup(id: EnginePeer.Id(0), title: title, photo: image.flatMap { [$0] } ?? [], participantCount: Int(memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) + let peer = TelegramGroup(id: EnginePeer.Id(0), title: content.title, photo: content.image.flatMap { [$0] } ?? [], participantCount: Int(content.memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) self.addSubnode(self.avatarNode) self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor) self.addSubnode(self.titleNode) - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: content.title, font: Font.semibold(24.0), textColor: theme.actionSheet.primaryTextColor) self.addSubnode(self.countNode) let membersString: String - if isGroup { - if !members.isEmpty { + if content.isGroup { + if case let .invite(_, _, _, memberCount, members) = content, !members.isEmpty { membersString = strings.Invitation_Members(memberCount) } else { - membersString = strings.Conversation_StatusMembers(memberCount) + membersString = strings.Conversation_StatusMembers(content.memberCount) } } else { - membersString = strings.Conversation_StatusSubscribers(memberCount) + membersString = strings.Conversation_StatusSubscribers(content.memberCount) } - self.countNode.attributedText = NSAttributedString(string: membersString, font: Font.regular(16.0), textColor: theme.actionSheet.secondaryTextColor) + self.countNode.attributedText = NSAttributedString(string: membersString, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor) if !self.peerNodes.isEmpty { for peerNode in peerNodes { @@ -90,6 +146,21 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.addSubnode(self.peersScrollNode) } self.moreNode.flatMap(self.peersScrollNode.addSubnode) + + if case let .request(isGroup, _, _, about, _) = content { + if let about = about, !about.isEmpty { + self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor) + self.addSubnode(self.aboutNode) + } + + self.descriptionNode.attributedText = NSAttributedString(string: isGroup ? strings.MemberRequests_RequestToJoinDescriptionGroup : strings.MemberRequests_RequestToJoinDescriptionChannel, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor, paragraphAlignment: .center) + self.addSubnode(self.descriptionNode) + } + + self.actionButtonNode.pressed = { [weak self] in + self?.join?() + } + self.addSubnode(self.actionButtonNode) } func activate() { @@ -106,19 +177,41 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { - let nodeHeight: CGFloat = self.peerNodes.isEmpty ? 224.0 : 324.0 + var nodeHeight: CGFloat = (self.peerNodes.isEmpty ? 264.0 : 364.0) + + let paddedSize = CGSize(width: size.width - 60.0, height: size.height) + + var aboutSize: CGSize? + var descriptionSize: CGSize? + if self.aboutNode.supernode != nil { + let measuredSize = self.aboutNode.measure(paddedSize) + nodeHeight += measuredSize.height + 20.0 + aboutSize = measuredSize + } + if self.descriptionNode.supernode != nil { + let measuredSize = self.descriptionNode.measure(paddedSize) + nodeHeight += measuredSize.height + 20.0 + 10.0 + descriptionSize = measuredSize + } let verticalOrigin = size.height - nodeHeight - let avatarSize: CGFloat = 75.0 + let avatarSize: CGFloat = 100.0 - transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 22.0), size: CGSize(width: avatarSize, height: avatarSize))) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 32.0), size: CGSize(width: avatarSize, height: avatarSize))) let titleSize = self.titleNode.measure(size) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0), size: titleSize)) let countSize = self.countNode.measure(size) - transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0 + titleSize.height + 1.0), size: countSize)) + transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0), size: countSize)) + + var verticalOffset = verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 18.0 + + if let aboutSize = aboutSize { + transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize)) + verticalOffset += aboutSize.height + 20.0 + } let peerSize = CGSize(width: 85.0, height: 95.0) let peerInset: CGFloat = 10.0 @@ -136,9 +229,22 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + (self.moreNode != nil ? peerSize.width : 0.0) + peerInset * 2.0, height: peerSize.height) - transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 168.0), size: CGSize(width: size.width, height: peerSize.height))) + transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 210.0), size: CGSize(width: size.width, height: peerSize.height))) - self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) + if !self.peerNodes.isEmpty { + verticalOffset += 100.0 + } + + let buttonInset: CGFloat = 16.0 + let actionButtonHeight = self.actionButtonNode.updateLayout(width: size.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.actionButtonNode, frame: CGRect(x: buttonInset, y: verticalOffset, width: size.width, height: actionButtonHeight)) + verticalOffset += actionButtonHeight + 20.0 + + if let descriptionSize = descriptionSize { + transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: verticalOffset), size: descriptionSize)) + } + + self.contentOffsetUpdated?(-size.height + nodeHeight, transition) } func updateSelectedPeers() { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 90fb25e0d3..d0004cc945 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -266,7 +266,9 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati } }; carouselItem.allowCaptions = true - carouselItem.editingContext.setForcedCaption(initialCaption, entities: []) + if !initialCaption.isEmpty { + carouselItem.editingContext.setForcedCaption(initialCaption, entities: []) + } itemViews.append(carouselItem) let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 0c3b7108de..22e5be3e65 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -60,7 +60,9 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co controller.shouldShowFileTipIfNeeded = showFileTooltip controller.requestSearchController = presentWebSearch - controller.editingContext.setForcedCaption(initialCaption, entities: []) + if !initialCaption.isEmpty { + controller.editingContext.setForcedCaption(initialCaption, entities: []) + } } public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> { diff --git a/submodules/LocationUI/Sources/LocationLiveListItem.swift b/submodules/LocationUI/Sources/LocationLiveListItem.swift index 22c561d19e..7dd222b5cd 100644 --- a/submodules/LocationUI/Sources/LocationLiveListItem.swift +++ b/submodules/LocationUI/Sources/LocationLiveListItem.swift @@ -14,6 +14,7 @@ import LocationResources import AppBundle import AvatarNode import LiveLocationTimerNode +import SolidRoundedButtonNode final class LocationLiveListItem: ListViewItem { let presentationData: ItemListPresentationData @@ -22,18 +23,33 @@ final class LocationLiveListItem: ListViewItem { let context: AccountContext let message: Message let distance: Double? + + let drivingTime: Double? + let transitTime: Double? + let walkingTime: Double? + let action: () -> Void let longTapAction: () -> Void - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: Message, distance: Double?, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }) { + let drivingAction: () -> Void + let transitAction: () -> Void + let walkingAction: () -> Void + + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: Message, distance: Double?, drivingTime: Double?, transitTime: Double?, walkingTime: Double?, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.context = context self.message = message self.distance = distance + self.drivingTime = drivingTime + self.transitTime = transitTime + self.walkingTime = walkingTime self.action = action self.longTapAction = longTapAction + self.drivingAction = drivingAction + self.transitAction = transitAction + self.walkingAction = walkingAction } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -84,6 +100,10 @@ final class LocationLiveListItemNode: ListViewItemNode { private let avatarNode: AvatarNode private var timerNode: ChatMessageLiveLocationTimerNode? + private var drivingButtonNode: SolidRoundedButtonNode? + private var transitButtonNode: SolidRoundedButtonNode? + private var walkingButtonNode: SolidRoundedButtonNode? + private var item: LocationLiveListItem? private var layoutParams: ListViewItemLayoutParams? @@ -99,7 +119,7 @@ final class LocationLiveListItemNode: ListViewItemNode { self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() - + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.backgroundNode) @@ -185,7 +205,10 @@ final class LocationLiveListItemNode: ListViewItemNode { let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 54.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = 1.0 - let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height) + var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height) + if item.drivingTime != nil || item.transitTime != nil || item.walkingTime != nil { + contentSize.height += 46.0 + } let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) return (nodeLayout, { [weak self] in @@ -217,6 +240,43 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.addSubnode(subtitleNode) } + let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) + if strongSelf.drivingButtonNode == nil { + strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsDriving"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.drivingButtonNode?.alpha = 0.0 + strongSelf.drivingButtonNode?.allowsGroupOpacity = true + strongSelf.drivingButtonNode?.pressed = { [weak self] in + if let item = self?.item { + item.drivingAction() + } + } + strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } + + strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsTransit"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.transitButtonNode?.alpha = 0.0 + strongSelf.transitButtonNode?.allowsGroupOpacity = true + strongSelf.transitButtonNode?.pressed = { [weak self] in + if let item = self?.item { + item.transitAction() + } + } + strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } + + strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsWalking"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.walkingButtonNode?.alpha = 0.0 + strongSelf.walkingButtonNode?.allowsGroupOpacity = true + strongSelf.walkingButtonNode?.pressed = { [weak self] in + if let item = self?.item { + item.walkingAction() + } + } + strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } + } else if let _ = updatedTheme { + strongSelf.drivingButtonNode?.updateTheme(buttonTheme) + strongSelf.transitButtonNode?.updateTheme(buttonTheme) + strongSelf.walkingButtonNode?.updateTheme(buttonTheme) + } + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame @@ -231,7 +291,7 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(peer), overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false) } - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize)) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) @@ -255,11 +315,48 @@ final class LocationLiveListItemNode: ListViewItemNode { } let timerSize = CGSize(width: 28.0, height: 28.0) timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(liveBroadcastingTimeout), strings: item.presentationData.strings) - timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0)), size: timerSize) + timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() } + + if let drivingTime = item.drivingTime { + strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) + + if currentItem?.drivingTime == nil { + strongSelf.drivingButtonNode?.alpha = 1.0 + strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + if let transitTime = item.transitTime { + strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 }) + + if currentItem?.transitTime == nil { + strongSelf.transitButtonNode?.alpha = 1.0 + strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + if let walkingTime = item.walkingTime { + strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) + + if currentItem?.walkingTime == nil { + strongSelf.walkingButtonNode?.alpha = 1.0 + strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + let directionsWidth: CGFloat = 93.0 + let directionsSpacing: CGFloat = 8.0 + let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + + strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight)) + strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset + directionsWidth + directionsSpacing, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight)) + strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset + directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight)) } }) }) diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index ae0e8f8f5a..3b615afe37 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -70,11 +70,11 @@ public func nearbyVenues(context: AccountContext, latitude: Double, longitude: D } } -func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> String? { - if eta > 0.0 && eta < 60.0 * 60.0 * 10.0 { - let eta = max(eta, 60.0) - let minutes = Int32(eta / 60.0) % 60 - let hours = Int32(eta / 3600.0) +func stringForEstimatedDuration(strings: PresentationStrings, time: Double, format: (String) -> String) -> String? { + if time > 0.0 { + let time = max(time, 60.0) + let minutes = Int32(time / 60.0) % 60 + let hours = Int32(time / 3600.0) let string: String if hours > 1 { @@ -86,7 +86,7 @@ func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> St } else { string = strings.Map_ETAMinutes(minutes) } - return strings.Map_DirectionsDriveEta(string).string + return format(string) } else { return nil } @@ -117,7 +117,7 @@ func throttledUserLocation(_ userLocation: Signal) -> Sign } } -func driveEta(coordinate: CLLocationCoordinate2D) -> Signal { +func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal { return Signal { subscriber in let destinationPlacemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil) let destination = MKMapItem(placemark: destinationPlacemark) @@ -125,7 +125,7 @@ func driveEta(coordinate: CLLocationCoordinate2D) -> Signal { let request = MKDirections.Request() request.source = MKMapItem.forCurrentLocation() request.destination = destination - request.transportType = .automobile + request.transportType = transportType request.requestsAlternateRoutes = false let directions = MKDirections(request: request) diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 758dbe9cf7..73e4dee521 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -15,6 +15,7 @@ import OpenInExternalAppUI import ShareController import DeviceAccess import UndoUI +import MapKit public class LocationViewParams { let sendLiveLocation: (TelegramMediaMap) -> Void @@ -43,7 +44,7 @@ class LocationViewInteraction { let updateMapMode: (LocationMapMode) -> Void let toggleTrackingMode: () -> Void let goToCoordinate: (CLLocationCoordinate2D) -> Void - let requestDirections: () -> Void + let requestDirections: (TelegramMediaMap, String?, OpenInLocationDirections) -> Void let share: () -> Void let setupProximityNotification: (Bool, MessageId?) -> Void let updateSendActionHighlight: (Bool) -> Void @@ -52,7 +53,7 @@ class LocationViewInteraction { let updateRightBarButton: (LocationViewRightBarButton) -> Void let present: (ViewController) -> Void - init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, MessageId?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, MessageId?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.toggleTrackingMode = toggleTrackingMode @@ -161,12 +162,31 @@ public final class LocationViewController: ViewController { state.selectedLocation = .coordinate(coordinate, false) return state } - }, requestDirections: { [weak self] in + }, requestDirections: { [weak self] location, peerName, directions in guard let strongSelf = self else { return } - if let location = getLocation(from: strongSelf.subject) { - strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil) + let item: OpenInItem = .location(location: location, directions: directions) + let openInOptions = availableOpenInOptions(context: context, item: item) + if openInOptions.count == 1, let action = openInOptions.first?.action() { + if case let .openLocation(latitude, longitude, directions) = action { + let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:]) + let mapItem = MKMapItem(placemark: placemark) + if let title = location.venue?.title { + mapItem.name = title + } else if let peerName = peerName { + mapItem.name = peerName + } + + if let directions = directions { + let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ] + MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options) + } else { + mapItem.openInMaps(launchOptions: nil) + } + } + } else { + strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: directions), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil) } }, share: { [weak self] in guard let strongSelf = self else { @@ -176,7 +196,7 @@ public final class LocationViewController: ViewController { let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: { strongSelf.present(ShareController(context: context, subject: .mapMedia(location), externalShare: true), in: .window(.root), with: nil) }) - strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil) + strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil) } }, setupProximityNotification: { [weak self] reset, messageId in guard let strongSelf = self else { diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 14088385a9..09eb9ad150 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -37,6 +37,7 @@ private struct LocationViewTransaction { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] + let gotTravelTimes: Bool } private enum LocationViewEntryId: Hashable { @@ -48,7 +49,7 @@ private enum LocationViewEntryId: Hashable { private enum LocationViewEntry: Comparable, Identifiable { case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?) case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?) - case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, Message, Double?, Int) + case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, Message, Double?, Double?, Double?, Double?, Int) var stableId: LocationViewEntryId { switch self { @@ -56,7 +57,7 @@ private enum LocationViewEntry: Comparable, Identifiable { return .info case .toggleLiveLocation: return .toggleLiveLocation - case let .liveLocation(_, _, _, message, _, _): + case let .liveLocation(_, _, _, message, _, _, _, _, _): return .liveLocation(message.stableId) } } @@ -75,8 +76,8 @@ private enum LocationViewEntry: Comparable, Identifiable { } else { return false } - case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsIndex): - if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsIndex == rhsIndex { + case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): + if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { return true } else { return false @@ -100,11 +101,11 @@ private enum LocationViewEntry: Comparable, Identifiable { case .liveLocation: return true } - case let .liveLocation(_, _, _, _, _, lhsIndex): + case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): switch rhs { case .info, .toggleLiveLocation: return false - case let .liveLocation(_, _, _, _, _, rhsIndex): + case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): return lhsIndex < rhsIndex } } @@ -125,11 +126,14 @@ private enum LocationViewEntry: Comparable, Identifiable { } else { distanceString = nil } - let eta = time.flatMap { stringForEstimatedDuration(strings: presentationData.strings, eta: $0) } + var eta: String? + if let time = time, time < 60.0 * 60.0 * 10.0 { + eta = stringForEstimatedDuration(strings: presentationData.strings, time: time, format: { presentationData.strings.Map_DirectionsDriveEta($0).string }) + } return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, eta: eta, action: { interaction?.goToCoordinate(location.coordinate) }, getDirections: { - interaction?.requestDirections() + interaction?.requestDirections(location, nil, .driving) }) case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout): let beginTimeAndTimeout: (Double, Double)? @@ -147,24 +151,40 @@ private enum LocationViewEntry: Comparable, Identifiable { }, highlighted: { highlight in interaction?.updateSendActionHighlight(highlight) }) - case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, _): - return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, action: { + case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): + var title: String? + if let author = message.author { + title = EnginePeer(author).displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) + } + return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { if let location = getLocation(from: message) { interaction?.goToCoordinate(location.coordinate) } - }, longTapAction: {}) + }, longTapAction: {}, drivingAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .driving) + } + }, transitAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .transit) + } + }, walkingAction: { + if let location = getLocation(from: message) { + interaction?.requestDirections(location, title, .walking) + } + }) } } } -private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> LocationViewTransaction { +private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?, gotTravelTimes: Bool) -> LocationViewTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates) + return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes) } enum LocationViewLocation: Equatable { @@ -217,6 +237,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan var reportedAnnotationsReady = false var onAnnotationsReady: (() -> Void)? + + private let travelDisposables = DisposableSet() + private var travelTimes: [EngineMessage.Id: (Double, Double?, Double?, Double?)] = [:] { + didSet { + self.travelTimesPromise.set(.single(self.travelTimes)) + } + } + private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, Double?, Double?, Double?)]>([:]) init(context: AccountContext, presentationData: PresentationData, subject: Message, interaction: LocationViewInteraction, locationManager: LocationManager) { self.context = context @@ -263,7 +291,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil { eta = .single(nil) - |> then(driveEta(coordinate: location.coordinate)) + |> then(getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile)) if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { address = .single(venueAddress) @@ -304,13 +332,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let previousUserAnnotation = Atomic(value: nil) let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) let previousEntries = Atomic<[LocationViewEntry]?>(value: nil) + let previousHadTravelTimes = Atomic(value: false) let selfPeer = context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(context.account.peerId) } - - self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta) - |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta in + + self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta, self.travelTimesPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta, travelTimes in if let strongSelf = self, let location = getLocation(from: subject) { var entries: [LocationViewEntry] = [] var annotations: [LocationPinAnnotation] = [] @@ -323,7 +352,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan var proximityNotificationRadius: Int32? var index: Int = 0 + var isLocationView = false if location.liveBroadcastingTimeout == nil { + isLocationView = true + let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } @@ -414,11 +446,45 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } + let timestamp = CACurrentMediaTime() if message.localTags.contains(.OutgoingLiveLocation), let selfPeer = selfPeer { userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: true, heading: location.heading) } else { + var drivingTime: Double? + var transitTime: Double? + var walkingTime: Double? + if !isLocationView { + if let (previousTimestamp, maybeDrivingTime, maybeTransitTime, maybeWalkingTime) = travelTimes[message.id] { + drivingTime = maybeDrivingTime + transitTime = maybeTransitTime + walkingTime = maybeWalkingTime + + if timestamp > previousTimestamp + 60.0 { + strongSelf.travelDisposables.add(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)).start(next: { [weak self] drivingTime, transitTime, walkingTime in + guard let strongSelf = self else { + return + } + let timestamp = CACurrentMediaTime() + var travelTimes = strongSelf.travelTimes + travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime) + strongSelf.travelTimes = travelTimes + })) + } + } else { + strongSelf.travelDisposables.add(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)).start(next: { [weak self] drivingTime, transitTime, walkingTime in + guard let strongSelf = self else { + return + } + let timestamp = CACurrentMediaTime() + var travelTimes = strongSelf.travelTimes + travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime) + strongSelf.travelTimes = travelTimes + })) + } + } + annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: message.author?.id == context.account.peerId, heading: location.heading)) - entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, index)) + entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, index)) } index += 1 } @@ -441,8 +507,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let previousEntries = previousEntries.swap(entries) let previousState = previousState.swap(state) - - let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction) + let previousHadTravelTimes = previousHadTravelTimes.swap(!travelTimes.isEmpty) + + let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction, gotTravelTimes: !travelTimes.isEmpty && !previousHadTravelTimes) strongSelf.enqueueTransition(transition) strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: false) @@ -581,7 +648,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan deinit { self.disposable?.dispose() - + self.travelDisposables.dispose() self.locationManager.manager.stopUpdatingHeading() } @@ -633,17 +700,20 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.enqueuedTransitions.remove(at: 0) let scrollToItem: ListViewScrollToItem? - if !self.initialized, transition.insertions.count > 0 { + if (!self.initialized && transition.insertions.count > 0) || transition.gotTravelTimes { var index: Int = 0 var offset: CGFloat = 0.0 - if transition.insertions.count > 2 { + if transition.gotTravelTimes { + index = 1 + offset = 0.0 + } else if transition.insertions.count > 2 { index = 2 offset = 40.0 } else if transition.insertions.count == 2 { index = 1 } - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(offset), animated: false, curve: .Default(duration: nil), directionHint: .Up) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(offset), animated: transition.gotTravelTimes, curve: .Default(duration: 0.3), directionHint: .Up) self.initialized = true } else { scrollToItem = nil diff --git a/submodules/NetworkLogging/Sources/NetworkLogging.m b/submodules/NetworkLogging/Sources/NetworkLogging.m index 9b84c551de..90bc12ba29 100644 --- a/submodules/NetworkLogging/Sources/NetworkLogging.m +++ b/submodules/NetworkLogging/Sources/NetworkLogging.m @@ -1,7 +1,7 @@ #import #import -#import +#import static void (*bridgingTrace)(NSString *, NSString *); void setBridgingTraceFunction(void (*f)(NSString *, NSString *)) { diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift b/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift index 81c55e162d..f6771facb0 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift @@ -54,12 +54,12 @@ public final class OpenInActionSheetController: ActionSheetController { switch action { case let .openUrl(url): openUrl(url) - case let .openLocation(latitude, longitude, withDirections): + case let .openLocation(latitude, longitude, directions): let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:]) let mapItem = MKMapItem(placemark: placemark) - if withDirections { - let options = [ MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving ] + if let directions = directions { + let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ] MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options) } else { mapItem.openInMaps(launchOptions: nil) diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index 339bdc1437..e359d7895b 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -8,7 +8,35 @@ import UrlEscaping public enum OpenInItem { case url(url: String) - case location(location: TelegramMediaMap, withDirections: Bool) + case location(location: TelegramMediaMap, directions: OpenInLocationDirections?) +} + +public enum OpenInLocationDirections: Equatable { + case walking + case driving + case transit + + var transportType: MKDirectionsTransportType { + switch self { + case .walking: + return .walking + case .transit: + return .transit + case .driving: + return .automobile + } + } + + public var launchOptions: String { + switch self { + case .walking: + return MKLaunchOptionsDirectionsModeWalking + case .transit: + return MKLaunchOptionsDirectionsModeTransit + case .driving: + return MKLaunchOptionsDirectionsModeDriving + } + } } public enum OpenInApplication: Equatable { @@ -20,7 +48,7 @@ public enum OpenInApplication: Equatable { public enum OpenInAction { case none case openUrl(url: String) - case openLocation(latitude: Double, longitude: Double, withDirections: Bool) + case openLocation(latitude: Double, longitude: Double, directions: OpenInLocationDirections?) } public final class OpenInOption { @@ -170,11 +198,11 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope options.append(OpenInOption(identifier: "alook", application: .other(title: "Alook Browser", identifier: 1261944766, scheme: "alook", store: nil), action: { return .openUrl(url: "alook://\(url)") })) - case let .location(location, withDirections): + case let .location(location, directions): let lat = location.latitude let lon = location.longitude - if !withDirections { + if directions == nil { if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "foursquare" { options.append(OpenInOption(identifier: "foursquare", application: .other(title: "Foursquare", identifier: 306934924, scheme: "foursquare", store: nil), action: { return .openUrl(url: "foursquare://venues/\(venueId)") @@ -183,13 +211,22 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope } options.append(OpenInOption(identifier: "appleMaps", application: .maps, action: { - return .openLocation(latitude: lat, longitude: lon, withDirections: withDirections) + return .openLocation(latitude: lat, longitude: lon, directions: directions) })) options.append(OpenInOption(identifier: "googleMaps", application: .other(title: "Google Maps", identifier: 585027354, scheme: "comgooglemaps-x-callback", store: nil), action: { let coordinates = "\(lat),\(lon)" - if withDirections { - return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=driving&x-success=telegram://?resume=true&x-source=Telegram") + if let directions = directions { + let directionsMode: String + switch directions { + case .walking: + directionsMode = "walking" + case .driving: + directionsMode = "driving" + case .transit: + directionsMode = "transit" + } + return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=\(directionsMode)&x-success=telegram://?resume=true&x-source=Telegram") } else { if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "gplaces" { return .openUrl(url: "https://www.google.com/maps/search/?api=1&query=\(venue.address ?? "")&query_place_id=\(venueId)") @@ -200,7 +237,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope })) options.append(OpenInOption(identifier: "yandexMaps", application: .other(title: "Yandex.Maps", identifier: 313877526, scheme: "yandexmaps", store: nil), action: { - if withDirections { + if let _ = directions { return .openUrl(url: "yandexmaps://build_route_on_map?lat_to=\(lat)&lon_to=\(lon)") } else { return .openUrl(url: "yandexmaps://maps.yandex.ru/?pt=\(lon),\(lat)&z=16") @@ -227,7 +264,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope return .openUrl(url: "lyft://ridetype?id=lyft&destination[latitude]=\(lat)&destination[longitude]=\(lon)") })) - if withDirections { + if let _ = directions { options.append(OpenInOption(identifier: "citymapper", application: .other(title: "Citymapper", identifier: 469463298, scheme: "citymapper", store: nil), action: { let endName: String let endAddress: String @@ -251,7 +288,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: { let coordinates = "\(lon),\(lat)" - if withDirections { + if let _ = directions { return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go") } else { return .openUrl(url: "dgis://2gis.ru/geo/\(coordinates)") @@ -259,7 +296,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope })) options.append(OpenInOption(identifier: "moovit", application: .other(title: "Moovit", identifier: 498477945, scheme: "moovit", store: nil), action: { - if withDirections { + if let _ = directions { let destName: String if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 { destName = title @@ -272,7 +309,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope } })) - if !withDirections { + if directions == nil { options.append(OpenInOption(identifier: "hereMaps", application: .other(title: "HERE Maps", identifier: 955837609, scheme: "here-location", store: nil), action: { return .openUrl(url: "here-location://\(lat),\(lon)") })) @@ -280,7 +317,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope options.append(OpenInOption(identifier: "waze", application: .other(title: "Waze", identifier: 323229106, scheme: "waze", store: nil), action: { let url = "waze://?ll=\(lat),\(lon)" - if withDirections { + if let _ = directions { return .openUrl(url: url.appending("&navigate=yes")) } else { return .openUrl(url: url) diff --git a/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m b/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m index 26b8fd9390..7c28f02c63 100644 --- a/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m +++ b/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m @@ -1,6 +1,6 @@ #import -#import +#import #import #import diff --git a/submodules/ShareItems/Impl/Sources/TGShareLocationSignals.m b/submodules/ShareItems/Impl/Sources/TGShareLocationSignals.m index 7eaf7fe418..58387a1d92 100644 --- a/submodules/ShareItems/Impl/Sources/TGShareLocationSignals.m +++ b/submodules/ShareItems/Impl/Sources/TGShareLocationSignals.m @@ -1,6 +1,6 @@ #import -#import +#import NSString *const TGShareAppleMapsHost = @"maps.apple.com"; NSString *const TGShareAppleMapsPath = @"/maps"; diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 44a0873bb5..d92751fc6a 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -23,6 +23,7 @@ public enum SolidRoundedButtonFont { public final class SolidRoundedButtonNode: ASDisplayNode { private var theme: SolidRoundedButtonTheme private var font: SolidRoundedButtonFont + private var fontSize: CGFloat private let buttonBackgroundNode: ASDisplayNode private let buttonGlossNode: SolidRoundedButtonGlossNode @@ -53,9 +54,10 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } } - public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { + public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme self.font = font + self.fontSize = fontSize self.buttonHeight = height self.buttonCornerRadius = cornerRadius self.title = title @@ -128,7 +130,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.buttonBackgroundNode.backgroundColor = theme.backgroundColor self.buttonGlossNode.color = theme.foregroundColor - self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(17.0) : Font.regular(17.0), textColor: theme.foregroundColor) + self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor) self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) if let width = self.validLayout { @@ -150,7 +152,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { transition.updateFrame(node: self.buttonNode, frame: buttonFrame) if self.title != self.titleNode.attributedText?.string { - self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(17.0) : Font.regular(17.0), textColor: self.theme.foregroundColor) + self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.theme.foregroundColor) } let iconSize = self.iconNode.image?.size ?? CGSize() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index d702b6ad2a..d4cab0dbff 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -893,6 +893,7 @@ private final class PeerInvitationImportersContextImpl { var results = self.results results.removeAll(where: { $0.peer.peerId == peerId}) self.results = results + self.count = max(0, self.count - 1) self.updateState() self.updateCache() } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 89c384e58c..c15f022315 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -33,7 +33,7 @@ public func dateFillNeedsBlur(theme: PresentationTheme, wallpaper: TelegramWallp public let defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) public let defaultPresentationTheme = makeDefaultDayPresentationTheme(serviceBackgroundColor: defaultServiceBackgroundColor, day: false, preview: false) -public let defaultDayAccentColor = UIColor(rgb: 0x007ee5) +public let defaultDayAccentColor = UIColor(rgb: 0x007aff) public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, outgoingAccentColor: UIColor?, backgroundColors: [UInt32], bubbleColors: [UInt32], animateBubbleColors: Bool?, wallpaper forcedWallpaper: TelegramWallpaper? = nil, serviceBackgroundColor: UIColor?) -> PresentationTheme { if (theme.referenceTheme != .day && theme.referenceTheme != .dayClassic) { @@ -346,7 +346,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let intro = PresentationThemeIntro( statusBarStyle: .black, primaryTextColor: UIColor(rgb: 0x000000), - accentTextColor: UIColor(rgb: 0x007ee5), + accentTextColor: defaultDayAccentColor, disabledTextColor: UIColor(rgb: 0xd0d0d0), startButtonColor: UIColor(rgb: 0x2ca5e0), dotColor: UIColor(rgb: 0xd9d9d9) @@ -358,12 +358,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio ) let rootNavigationBar = PresentationThemeRootNavigationBar( - buttonColor: UIColor(rgb: 0x007ee5), + buttonColor: defaultDayAccentColor, disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x787878), controlColor: UIColor(rgb: 0x7e8791), - accentTextColor: UIColor(rgb: 0x007ee5), + accentTextColor: defaultDayAccentColor, blurredBackgroundColor: UIColor(rgb: 0xf2f2f2, alpha: 0.9), opaqueBackgroundColor: UIColor(rgb: 0xf7f7f7).mixedWith(.white, alpha: 0.14), separatorColor: UIColor(rgb: 0xc8c7cc), @@ -382,9 +382,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio backgroundColor: rootNavigationBar.blurredBackgroundColor, separatorColor: UIColor(rgb: 0xa3a3a3), iconColor: UIColor(rgb: 0x959595), - selectedIconColor: UIColor(rgb: 0x007ee5), + selectedIconColor: defaultDayAccentColor, textColor: UIColor(rgb: 0x959595), - selectedTextColor: UIColor(rgb: 0x007ee5), + selectedTextColor: defaultDayAccentColor, badgeBackgroundColor: UIColor(rgb: 0xff3b30), badgeStrokeColor: UIColor(rgb: 0xff3b30), badgeTextColor: UIColor(rgb: 0xffffff) @@ -392,7 +392,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let navigationSearchBar = PresentationThemeNavigationSearchBar( backgroundColor: UIColor(rgb: 0xffffff), - accentColor: UIColor(rgb: 0x007ee5), + accentColor: defaultDayAccentColor, inputFillColor: UIColor(rgb: 0x000000, alpha: 0.06), inputTextColor: UIColor(rgb: 0x000000), inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), @@ -423,7 +423,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio itemPrimaryTextColor: UIColor(rgb: 0x000000), itemSecondaryTextColor: UIColor(rgb: 0x8e8e93), itemDisabledTextColor: UIColor(rgb: 0x8e8e93), - itemAccentColor: UIColor(rgb: 0x007ee5), + itemAccentColor: defaultDayAccentColor, itemHighlightedColor: UIColor(rgb: 0x00b12c), itemDestructiveColor: UIColor(rgb: 0xff3b30), itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), @@ -443,12 +443,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xf09a37), foregroundColor: UIColor(rgb: 0xffffff)), destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3824), foregroundColor: UIColor(rgb: 0xffffff)), constructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x00c900), foregroundColor: UIColor(rgb: 0xffffff)), - accent: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x007ee5), foregroundColor: UIColor(rgb: 0xffffff)), + accent: PresentationThemeFillForeground(fillColor: defaultDayAccentColor, foregroundColor: UIColor(rgb: 0xffffff)), warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff9500), foregroundColor: UIColor(rgb: 0xffffff)), inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: UIColor(rgb: 0xffffff)) ), itemCheckColors: PresentationThemeFillStrokeForeground( - fillColor: UIColor(rgb: 0x007ee5), + fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff) ), @@ -471,7 +471,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio scrollIndicatorColor: UIColor(white: 0.0, alpha: 0.3), pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7), inputClearButtonColor: UIColor(rgb: 0xcccccc), - itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)), + itemBarChart: PresentationThemeItemBarChart(color1: defaultDayAccentColor, color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)), itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb)), paymentOption: PresentationThemeList.PaymentOption( inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1), @@ -495,12 +495,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio messageTextColor: UIColor(rgb: 0x8e8e93), messageHighlightedTextColor: UIColor(rgb: 0x000000), messageDraftTextColor: UIColor(rgb: 0xdd4b39), - checkmarkColor: day ? UIColor(rgb: 0x007ee5) : UIColor(rgb: 0x21c004), + checkmarkColor: day ? defaultDayAccentColor : UIColor(rgb: 0x21c004), pendingIndicatorColor: UIColor(rgb: 0x8e8e93), failedFillColor: UIColor(rgb: 0xff3b30), failedForegroundColor: UIColor(rgb: 0xffffff), muteIconColor: UIColor(rgb: 0xa7a7ad), - unreadBadgeActiveBackgroundColor: UIColor(rgb: 0x007ee5), + unreadBadgeActiveBackgroundColor: defaultDayAccentColor, unreadBadgeActiveTextColor: UIColor(rgb: 0xffffff), unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb), unreadBadgeInactiveTextColor: UIColor(rgb: 0xffffff), @@ -509,7 +509,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio regularSearchBarColor: UIColor(rgb: 0xe9e9e9), sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), - verifiedIconFillColor: UIColor(rgb: 0x007ee5), + verifiedIconFillColor: defaultDayAccentColor, verifiedIconForegroundColor: UIColor(rgb: 0xffffff), secretIconColor: UIColor(rgb: 0x00b12c), pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)), @@ -530,13 +530,13 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), - linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3), + linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3), scamColor: UIColor(rgb: 0xff3b30), textHighlightColor: UIColor(rgb: 0xffe438), - accentTextColor: UIColor(rgb: 0x007ee5), - accentControlColor: UIColor(rgb: 0x007ee5), + accentTextColor: defaultDayAccentColor, + accentControlColor: defaultDayAccentColor, accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6), - mediaActiveControlColor: UIColor(rgb: 0x007ee5), + mediaActiveControlColor: defaultDayAccentColor, mediaInactiveControlColor: UIColor(rgb: 0xcacaca), mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), @@ -544,15 +544,15 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio fileDescriptionColor: UIColor(rgb: 0x999999), fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), mediaPlaceholderColor: UIColor(rgb: 0xe8ecf0), - polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.08), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x2dba45), barNegative: UIColor(rgb: 0xFE3824)), + polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: defaultDayAccentColor, highlight: defaultDayAccentColor.withAlphaComponent(0.08), separator: UIColor(rgb: 0xc8c7cc), bar: defaultDayAccentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x2dba45), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), - actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0x007ee5)), + actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.2), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil)), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), linkTextColor: UIColor(rgb: 0x004bad), - linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3), + linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3), scamColor: UIColor(rgb: 0xff3b30), textHighlightColor: UIColor(rgb: 0xffe438), accentTextColor: UIColor(rgb: 0x00a700), @@ -582,7 +582,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)), mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), - selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), + selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6), stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.25)), @@ -595,28 +595,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), - linkHighlightColor: UIColor(rgb: 0x007ee5, alpha: 0.3), + linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3), scamColor: UIColor(rgb: 0xff3b30), textHighlightColor: UIColor(rgb: 0xffc738), - accentTextColor: UIColor(rgb: 0x007ee5), - accentControlColor: UIColor(rgb: 0x007ee5), + accentTextColor: defaultDayAccentColor, + accentControlColor: defaultDayAccentColor, accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6), - mediaActiveControlColor: UIColor(rgb: 0x007ee5), + mediaActiveControlColor: defaultDayAccentColor, mediaInactiveControlColor: UIColor(rgb: 0xcacaca), mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), - fileTitleColor: UIColor(rgb: 0x007ee5), + fileTitleColor: defaultDayAccentColor, fileDescriptionColor: UIColor(rgb: 0x999999), fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.95), - polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.12), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), + polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: defaultDayAccentColor, highlight: defaultDayAccentColor.withAlphaComponent(0.12), separator: UIColor(rgb: 0xc8c7cc), bar: defaultDayAccentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), - actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)), - actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), - textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.3), - textSelectionKnobColor: UIColor(rgb: 0x007ee5)), + actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: defaultDayAccentColor), + actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), + textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.3), + textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), UIColor(rgb: 0x007ee5)], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), UIColor(rgb: 0x007ee5)], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)), + bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65), linkTextColor: UIColor(rgb: 0xffffff), @@ -636,8 +636,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio mediaPlaceholderColor: UIColor(rgb: 0x0077d9), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff, alpha: 0.65), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.65), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), - actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)), - actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), + actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: defaultDayAccentColor), + actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil)), @@ -648,9 +648,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0xe5e5ea)), - shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), + shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), - selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), + selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6), stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor.withAlphaComponent(0.3), withoutWallpaper: UIColor(rgb: 0xf7f7f7)), @@ -674,8 +674,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio ) let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl( - buttonColor: UIColor(rgb: 0x007ee5), - micLevelColor: UIColor(rgb: 0x007ee5, alpha: 0.2), + buttonColor: defaultDayAccentColor, + micLevelColor: defaultDayAccentColor.withAlphaComponent(0.2), activeIconColor: UIColor(rgb: 0xffffff) ) @@ -683,7 +683,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio panelBackgroundColor: rootNavigationBar.blurredBackgroundColor, panelBackgroundColorNoWallpaper: rootNavigationBar.blurredBackgroundColor, panelSeparatorColor: UIColor(rgb: 0xb2b2b2), - panelControlAccentColor: UIColor(rgb: 0x007ee5), + panelControlAccentColor: defaultDayAccentColor, panelControlColor: UIColor(rgb: 0x858e99), panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5), panelControlDestructiveColor: UIColor(rgb: 0xff3b30), @@ -692,7 +692,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio inputPlaceholderColor: UIColor(rgb: 0xbebec0), inputTextColor: UIColor(rgb: 0x000000), inputControlColor: UIColor(rgb: 0xa0a7b0), - actionControlFillColor: UIColor(rgb: 0x007ee5), + actionControlFillColor: defaultDayAccentColor, actionControlForegroundColor: UIColor(rgb: 0xffffff), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x8e8e93), @@ -727,8 +727,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio fillColor: UIColor(rgb: 0xf7f7f7), strokeColor: UIColor(rgb: 0xc8c7cc), foregroundColor: UIColor(rgb: 0x88888d), - badgeBackgroundColor: UIColor(rgb: 0x007ee5), - badgeStrokeColor: UIColor(rgb: 0x007ee5), + badgeBackgroundColor: defaultDayAccentColor, + badgeStrokeColor: defaultDayAccentColor, badgeTextColor: UIColor(rgb: 0xffffff) ) @@ -753,12 +753,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0), itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7), opaqueItemSeparatorColor: UIColor(white: 0.9, alpha: 1.0), - standardActionTextColor: UIColor(rgb: 0x007ee5), + standardActionTextColor: defaultDayAccentColor, destructiveActionTextColor: UIColor(rgb: 0xff3b30), disabledActionTextColor: UIColor(rgb: 0xb3b3b3), primaryTextColor: UIColor(rgb: 0x000000), - secondaryTextColor: UIColor(rgb: 0x5e5e5e), - controlAccentColor: UIColor(rgb: 0x007ee5), + secondaryTextColor: UIColor(rgb: 0x8e8e93), + controlAccentColor: defaultDayAccentColor, inputBackgroundColor: UIColor(rgb: 0xe9e9e9), inputHollowBackgroundColor: UIColor(rgb: 0xffffff), inputBorderColor: UIColor(rgb: 0xe4e4e6), @@ -778,7 +778,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio primaryColor: UIColor(rgb: 0x000000), secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8), destructiveColor: UIColor(rgb: 0xff3b30), - badgeFillColor: UIColor(rgb: 0x007ee5), + badgeFillColor: defaultDayAccentColor, badgeForegroundColor: UIColor(rgb: 0xffffff), badgeInactiveFillColor: UIColor(rgb: 0xb6b6bb), badgeInactiveForegroundColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json new file mode 100644 index 0000000000..ea6514ba62 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "car.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png new file mode 100644 index 0000000000..ae154703b4 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json new file mode 100644 index 0000000000..91af4d8829 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "transit.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png new file mode 100644 index 0000000000..73c9cf47c9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json new file mode 100644 index 0000000000..d07f928364 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "walking.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png new file mode 100644 index 0000000000..0376c5ddc8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png differ diff --git a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js new file mode 100644 index 0000000000..20b3fe89e7 --- /dev/null +++ b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js @@ -0,0 +1,108 @@ +var uiWebview_SearchResultCount = 0; + +/*! + @method uiWebview_HighlightAllOccurencesOfStringForElement + @abstract // helper function, recursively searches in elements and their child nodes + @discussion // helper function, recursively searches in elements and their child nodes + + element - HTML elements + keyword - string to search + */ + +function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { + if (element) { + if (element.nodeType == 3) { // Text node + var count = 0; + var elementTmp = element; + while (true) { + var value = elementTmp.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; + + count++; + elementTmp = document.createTextNode(value.substr(idx+keyword.length)); + } + + uiWebview_SearchResultCount += count; + + var index = uiWebview_SearchResultCount; + while (true) { + var value = element.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; // not found, abort + + var span = document.createElement("span"); + var text = document.createTextNode(value.substr(idx,keyword.length)); + span.appendChild(text); + + span.setAttribute("class","uiWebviewHighlight"); + span.style.backgroundColor="#ffe438"; + span.style.color="black"; + span.style.borderRadius="3px"; + + index--; + span.setAttribute("id", "SEARCH WORD"+(index)); + + text = document.createTextNode(value.substr(idx+keyword.length)); + element.deleteData(idx, value.length - idx); + + var next = element.nextSibling; + element.parentNode.insertBefore(span, next); + element.parentNode.insertBefore(text, next); + element = text; + } + + + } else if (element.nodeType == 1) { // Element node + if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') { + for (var i=element.childNodes.length-1; i>=0; i--) { + uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword); + } + } + } + } +} + +// the main entry point to start the search +function uiWebview_HighlightAllOccurencesOfString(keyword) { + uiWebview_RemoveAllHighlights(); + uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase()); +} + +// helper function, recursively removes the highlights in elements and their childs +function uiWebview_RemoveAllHighlightsForElement(element) { + if (element) { + if (element.nodeType == 1) { + if (element.getAttribute("class") == "uiWebviewHighlight") { + var text = element.removeChild(element.firstChild); + element.parentNode.insertBefore(text,element); + element.parentNode.removeChild(element); + return true; + } else { + var normalize = false; + for (var i=element.childNodes.length-1; i>=0; i--) { + if (uiWebview_RemoveAllHighlightsForElement(element.childNodes[i])) { + normalize = true; + } + } + if (normalize) { + element.normalize(); + } + } + } + } + return false; +} + +// the main entry point to remove the highlights +function uiWebview_RemoveAllHighlights() { + uiWebview_SearchResultCount = 0; + uiWebview_RemoveAllHighlightsForElement(document.body); +} + +function uiWebview_ScrollTo(idx) { + var scrollTo = document.getElementById("SEARCH WORD" + idx); + if (scrollTo) scrollTo.scrollIntoView(); +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7e96600ef8..36afbbce6b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -484,6 +484,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var nextChannelToReadDisposable: Disposable? + private var inviteRequestsContext: PeerInvitationImportersContext? + private var inviteRequestsDisposable = MetaDisposable() + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -4160,6 +4163,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.addMemberDisposable.dispose() self.importStateDisposable?.dispose() self.nextChannelToReadDisposable?.dispose() + self.inviteRequestsDisposable.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -4729,6 +4733,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var callsPrivate: Bool = false var slowmodeState: ChatSlowmodeState? var activeGroupCallInfo: ChatActiveGroupCallInfo? + var inviteRequestsPending: Int32? if let cachedData = cachedData as? CachedChannelData { pinnedMessageId = cachedData.pinnedMessageId if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout { @@ -4741,6 +4746,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let activeCall = cachedData.activeCall { activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) } + inviteRequestsPending = cachedData.inviteRequestsPending } else if let cachedData = cachedData as? CachedUserData { peerIsBlocked = cachedData.isBlocked callsAvailable = cachedData.voiceCallsAvailable @@ -4751,6 +4757,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let activeCall = cachedData.activeCall { activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) } + inviteRequestsPending = cachedData.inviteRequestsPending } else if let _ = cachedData as? CachedSecretChatData { } @@ -4782,7 +4789,63 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate - + + if let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0, strongSelf.inviteRequestsContext == nil { + let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil) + strongSelf.inviteRequestsContext = inviteRequestsContext + + strongSelf.inviteRequestsDisposable.set((inviteRequestsContext.state + |> deliverOnMainQueue).start(next: { [weak self] requestsState in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in + return state + .updatedTitlePanelContext({ context in + if requestsState.count > 0 { + let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3)) + if !context.contains(where: { + switch $0 { + case .inviteRequests(peers, requestsState.count): + return true + default: + return false + } + }) { + var updatedContexts = context.filter { c in + if case .inviteRequests = c { + return false + } else { + return true + } + } + updatedContexts.append(.inviteRequests(peers, requestsState.count)) + return updatedContexts.sorted() + } else { + return context + } + } else { + if let index = context.firstIndex(where: { + switch $0 { + case .inviteRequests: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.remove(at: index) + return updatedContexts + } else { + return context + } + } + }) + .updatedSlowmodeState(slowmodeState) + }) + })) + } + if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo { strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in return state @@ -7269,6 +7332,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0.updatedShowCommands(f($0.showCommands)) }) } + }, openInviteRequests: { [weak self] in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + let controller = inviteRequestsController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, existingContext: strongSelf.inviteRequestsContext) + controller.navigationPresentation = .modal + strongSelf.push(controller) + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) do { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index b5d2ef2016..b51eb290d0 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -40,7 +40,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat break loop } } - case .chatInfo, .requestInProgress, .toastAlert: + case .chatInfo, .requestInProgress, .toastAlert, .inviteRequests: selectedContext = context break loop } @@ -119,6 +119,16 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat panel.interfaceInteraction = interfaceInteraction return panel } + case let .inviteRequests(peers, count): + if let currentPanel = currentPanel as? ChatInviteRequestsTitlePanelNode { + currentPanel.update(peers: peers, count: count) + return currentPanel + } else { + let panel = ChatInviteRequestsTitlePanelNode(context: context) + panel.interfaceInteraction = interfaceInteraction + panel.update(peers: peers, count: count) + return panel + } } } diff --git a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift new file mode 100644 index 0000000000..c1757f8ed4 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift @@ -0,0 +1,210 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import TelegramPresentationData +import LocalizedPeerData +import TelegramStringFormatting +import AnimatedAvatarSetNode +import AccountContext + +private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode { + private var theme: PresentationTheme? + + private let labelNode: ImmediateTextNode + private let filledBackgroundNode: LinkHighlightingNode + + private let openPeersNearby: () -> Void + + init(openPeersNearby: @escaping () -> Void) { + self.openPeersNearby = openPeersNearby + + self.labelNode = ImmediateTextNode() + self.labelNode.maximumNumberOfLines = 1 + self.labelNode.textAlignment = .center + + self.filledBackgroundNode = LinkHighlightingNode(color: .clear) + + super.init() + + self.addSubnode(self.filledBackgroundNode) + self.addSubnode(self.labelNode) + } + + override func didLoad() { + super.didLoad() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.view.addGestureRecognizer(tapRecognizer) + } + + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + self.openPeersNearby() + } + + func update(width: CGFloat, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, chatPeer: Peer, distance: Int32, transition: ContainedViewLayoutTransition) -> CGFloat { + let primaryTextColor = serviceMessageColorComponents(theme: theme, wallpaper: wallpaper).primaryText + + if self.theme !== theme { + self.theme = theme + + self.labelNode.linkHighlightColor = primaryTextColor.withAlphaComponent(0.3) + } + + let topInset: CGFloat = 6.0 + let bottomInset: CGFloat = 6.0 + let sideInset: CGFloat = 16.0 + + let stringAndRanges = strings.Conversation_PeerNearbyDistance(EnginePeer(chatPeer).compactDisplayTitle, shortStringForDistance(strings: strings, distance: distance)) + + let attributedString = NSMutableAttributedString(string: stringAndRanges.string, font: Font.regular(13.0), textColor: primaryTextColor) + + let boldAttributes = [NSAttributedString.Key.font: Font.semibold(13.0), NSAttributedString.Key(rawValue: "_Link"): true as NSNumber] + for range in stringAndRanges.ranges.prefix(1) { + attributedString.addAttributes(boldAttributes, range: range.range) + } + + self.labelNode.attributedText = attributedString + let labelLayout = self.labelNode.updateLayoutFullInfo(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + var labelRects = labelLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + labelRects[index].size.width = labelRects[index + j].size.width + } + } + } + } + } + for i in 0 ..< labelRects.count { + labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) + labelRects[i].size.height = 20.0 + labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) + } + + let backgroundLayout = self.filledBackgroundNode.asyncLayout() + let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: wallpaper) + let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0) + backgroundApply() + + let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) + + let labelFrame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: topInset + floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) + self.labelNode.frame = labelFrame + self.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + + return topInset + backgroundSize.height + bottomInset + } +} + +final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { + private let context: AccountContext + + private let separatorNode: ASDisplayNode + + private let closeButton: HighlightableButtonNode + private var button: UIButton? + + private let avatarsContext: AnimatedAvatarSetContext + private var avatarsContent: AnimatedAvatarSetContext.Content? + private let avatarsNode: AnimatedAvatarSetNode + + private var theme: PresentationTheme? + + init(context: AccountContext) { + self.context = context + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.closeButton = HighlightableButtonNode() + self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.closeButton.displaysAsynchronously = false + + self.avatarsContext = AnimatedAvatarSetContext() + self.avatarsNode = AnimatedAvatarSetNode() + + super.init() + + self.addSubnode(self.separatorNode) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.avatarsNode) + } + + private var requestsCount: Int32 = 0 + func update(peers: [EnginePeer], count: Int32) { + self.requestsCount = count + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.avatarsContent = self.avatarsContext.update(peers: peers, animated: false) + self.button?.setTitle(presentationData.strings.Conversation_RequestsToJoin(count), for: []) + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { + if interfaceState.theme !== self.theme { + self.theme = interfaceState.theme + + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: []) + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + } + + let panelHeight: CGFloat = 40.0 + + let contentRightInset: CGFloat = 14.0 + rightInset + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + + if self.button == nil { + let view = UIButton() + view.titleLabel?.font = Font.regular(16.0) + view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor, for: []) + view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7), for: [.highlighted]) + view.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) + self.view.addSubview(view) + self.button = view + } + + self.button?.setTitle(interfaceState.strings.Conversation_RequestsToJoin(self.requestsCount), for: []) + + let maxInset = max(contentRightInset, leftInset) + let buttonWidth = floor(width - maxInset * 2.0) + self.button?.frame = CGRect(origin: CGPoint(x: maxInset, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) + + let initialPanelHeight = panelHeight + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + if let avatarsContent = self.avatarsContent { + let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true) + transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: leftInset + 8.0, y: floor((panelHeight - avatarsSize.height) / 2.0)), size: avatarsSize)) + } + + return LayoutResult(backgroundHeight: initialPanelHeight, insetHeight: panelHeight) + } + + @objc func buttonPressed() { + self.interfaceInteraction?.openInviteRequests() + } + + @objc func closePressed() { +// self.interfaceInteraction?.dismissReportPeer() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.closeButton.hitTest(CGPoint(x: point.x - self.closeButton.frame.minX, y: point.y - self.closeButton.frame.minY), with: event) { + return result + } + return super.hitTest(point, with: event) + } +} diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index df09108285..78581ff614 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -132,6 +132,7 @@ final class ChatPanelInterfaceInteraction { let presentInviteMembers: () -> Void let presentGigagroupHelp: () -> Void let updateShowCommands: ((Bool) -> Bool) -> Void + let openInviteRequests: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? init( @@ -218,6 +219,7 @@ final class ChatPanelInterfaceInteraction { presentGigagroupHelp: @escaping () -> Void, editMessageMedia: @escaping (MessageId, Bool) -> Void, updateShowCommands: @escaping ((Bool) -> Bool) -> Void, + openInviteRequests: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses? ) { self.setupReplyMessage = setupReplyMessage @@ -303,6 +305,7 @@ final class ChatPanelInterfaceInteraction { self.presentInviteMembers = presentInviteMembers self.presentGigagroupHelp = presentGigagroupHelp self.updateShowCommands = updateShowCommands + self.openInviteRequests = openInviteRequests self.statuses = statuses } } diff --git a/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift index 5b7fd2257f..2fe49532bc 100644 --- a/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift @@ -82,6 +82,7 @@ enum ChatTitlePanelContext: Equatable, Comparable { case chatInfo case requestInProgress case toastAlert(String) + case inviteRequests([EnginePeer], Int32) private var index: Int { switch self { @@ -93,6 +94,8 @@ enum ChatTitlePanelContext: Equatable, Comparable { return 2 case .toastAlert: return 3 + case .inviteRequests: + return 4 } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index b3bee8ce40..d1b1393f73 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -144,7 +144,9 @@ final class ChatRecentActionsController: TelegramBaseController { }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in - }, updateShowCommands: { _ in }, statuses: nil) + }, updateShowCommands: { _ in + }, openInviteRequests: { + }, statuses: nil) self.navigationItem.titleView = self.titleView diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index adfe38a53a..ef19c33b1b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -181,6 +181,7 @@ final class PeerInfoScreenData { let encryptionKeyFingerprint: SecretChatKeyFingerprint? let globalSettings: TelegramGlobalSettings? let invitations: PeerExportedInvitationsState? + let requests: PeerInvitationImportersState? init( peer: Peer?, @@ -195,7 +196,8 @@ final class PeerInfoScreenData { members: PeerInfoMembersData?, encryptionKeyFingerprint: SecretChatKeyFingerprint?, globalSettings: TelegramGlobalSettings?, - invitations: PeerExportedInvitationsState? + invitations: PeerExportedInvitationsState?, + requests: PeerInvitationImportersState? ) { self.peer = peer self.cachedData = cachedData @@ -210,6 +212,7 @@ final class PeerInfoScreenData { self.encryptionKeyFingerprint = encryptionKeyFingerprint self.globalSettings = globalSettings self.invitations = invitations + self.requests = requests } } @@ -433,7 +436,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, members: nil, encryptionKeyFingerprint: nil, globalSettings: globalSettings, - invitations: nil + invitations: nil, + requests: nil ) } } @@ -456,7 +460,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil, encryptionKeyFingerprint: nil, globalSettings: nil, - invitations: nil + invitations: nil, + requests: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -596,7 +601,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil, encryptionKeyFingerprint: encryptionKeyFingerprint, globalSettings: nil, - invitations: nil + invitations: nil, + requests: nil ) } case .channel: @@ -620,15 +626,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let invitationsContextPromise = Promise(nil) let invitationsStatePromise = Promise(nil) + let requestsContextPromise = Promise(nil) + let requestsStatePromise = Promise(nil) + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId), context.account.postbox.combinedView(keys: combinedKeys), status, invitationsContextPromise.get(), - invitationsStatePromise.get() + invitationsStatePromise.get(), + requestsContextPromise.get(), + requestsStatePromise.get() ) - |> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations -> PeerInfoScreenData in + |> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations, currentRequestsContext, requests -> PeerInfoScreenData in var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { @@ -641,17 +652,25 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen discussionPeer = peer } + var canManageInvitations = false + if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let _ = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { + canManageInvitations = true + } if currentInvitationsContext == nil { - var canManageInvitations = false - if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let _ = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { - canManageInvitations = true - } if canManageInvitations { let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) invitationsContextPromise.set(.single(invitationsContext)) invitationsStatePromise.set(invitationsContext.state |> map(Optional.init)) } } + + if currentRequestsContext == nil { + if canManageInvitations { + let requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil) + requestsContextPromise.set(.single(requestsContext)) + requestsStatePromise.set(requestsContext.state |> map(Optional.init)) + } + } return PeerInfoScreenData( peer: peerView.peers[peerId], @@ -666,7 +685,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil, encryptionKeyFingerprint: nil, globalSettings: nil, - invitations: invitations + invitations: invitations, + requests: requests ) } case let .group(groupId): @@ -767,6 +787,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let invitationsContextPromise = Promise(nil) let invitationsStatePromise = Promise(nil) + let requestsContextPromise = Promise(nil) + let requestsStatePromise = Promise(nil) + return combineLatest(queue: .mainQueue(), context.account.viewTracker.peerView(groupId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: groupId), @@ -774,9 +797,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen status, membersData, invitationsContextPromise.get(), - invitationsStatePromise.get() + invitationsStatePromise.get(), + requestsContextPromise.get(), + requestsStatePromise.get() ) - |> map { peerView, availablePanes, combinedView, status, membersData, currentInvitationsContext, invitations -> PeerInfoScreenData in + |> map { peerView, availablePanes, combinedView, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests -> PeerInfoScreenData in var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { @@ -798,23 +823,31 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } } - if currentInvitationsContext == nil { - var canManageInvitations = false - if let group = peerViewMainPeer(peerView) as? TelegramGroup { - if case .creator = group.role { - canManageInvitations = true - } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) { - canManageInvitations = true - } - } else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { + var canManageInvitations = false + if let group = peerViewMainPeer(peerView) as? TelegramGroup { + if case .creator = group.role { + canManageInvitations = true + } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) { canManageInvitations = true } + } else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { + canManageInvitations = true + } + if currentInvitationsContext == nil { if canManageInvitations { let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) invitationsContextPromise.set(.single(invitationsContext)) invitationsStatePromise.set(invitationsContext.state |> map(Optional.init)) } } + + if currentRequestsContext == nil { + if canManageInvitations { + let requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil) + requestsContextPromise.set(.single(requestsContext)) + requestsStatePromise.set(requestsContext.state |> map(Optional.init)) + } + } return PeerInfoScreenData( peer: peerView.peers[groupId], @@ -829,7 +862,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: membersData, encryptionKeyFingerprint: nil, globalSettings: nil, - invitations: invitations + invitations: invitations, + requests: requests ) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1ec09db815..ce59e54cf1 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -454,7 +454,9 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in - }, updateShowCommands: { _ in }, statuses: nil) + }, updateShowCommands: { _ in + }, openInviteRequests: { + }, statuses: nil) self.selectionPanel.interfaceInteraction = interfaceInteraction @@ -494,6 +496,7 @@ private enum PeerInfoParticipantsSection { case members case admins case banned + case memberRequests } private enum PeerInfoMemberAction { @@ -1034,7 +1037,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let ItemAbout = 2 let ItemAdmins = 3 let ItemMembers = 4 - let ItemBanned = 5 + let ItemMemberRequests = 5 + let ItemBanned = 6 let ItemLocationHeader = 7 let ItemLocation = 8 @@ -1105,6 +1109,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: { interaction.openParticipantsSection(.members) })) + + if let count = data.requests?.count, count > 0 { + items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: { + interaction.openParticipantsSection(.memberRequests) + })) + } + items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: { interaction.openParticipantsSection(.banned) })) @@ -4711,6 +4722,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } case .banned: self.controller?.push(channelBlacklistController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) + case .memberRequests: + self.controller?.push(inviteRequestsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) } } diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index a3a014ab05..960cf65ad1 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -300,7 +300,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in - }, updateShowCommands: { _ in }, statuses: nil) + }, updateShowCommands: { _ in + }, openInviteRequests: { + }, statuses: nil) self.readyValue.set(self.chatListNode.ready) } diff --git a/submodules/TgVoipWebrtc/BUILD b/submodules/TgVoipWebrtc/BUILD index a5203c7319..7a09039abf 100644 --- a/submodules/TgVoipWebrtc/BUILD +++ b/submodules/TgVoipWebrtc/BUILD @@ -8,6 +8,7 @@ objc_library( "Sources/**/*.mm", "Sources/**/*.h", "tgcalls/tgcalls/**/*.h", + "tgcalls/tgcalls/**/*.hpp", "tgcalls/tgcalls/**/*.cpp", "tgcalls/tgcalls/**/*.mm", "tgcalls/tgcalls/**/*.m", diff --git a/third-party/ogg/BUILD b/third-party/ogg/BUILD index 4d762b9bb1..1da3eaa02d 100644 --- a/third-party/ogg/BUILD +++ b/third-party/ogg/BUILD @@ -5,6 +5,7 @@ objc_library( module_name = "ogg", srcs = glob([ "Sources/*.c", + "Sources/*.h", ]), hdrs = glob([ "include/ogg/*.h", diff --git a/third-party/opusfile/BUILD b/third-party/opusfile/BUILD index 2af330f73d..4e210060c4 100644 --- a/third-party/opusfile/BUILD +++ b/third-party/opusfile/BUILD @@ -5,6 +5,7 @@ objc_library( module_name = "opusfile", srcs = glob([ "Sources/*.c", + "Sources/*.h", ]), hdrs = glob([ "include/opusfile/*.h", diff --git a/third-party/webrtc/BUILD b/third-party/webrtc/BUILD index 958f1394da..acb81a0030 100644 --- a/third-party/webrtc/BUILD +++ b/third-party/webrtc/BUILD @@ -34,6 +34,18 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/container/internal/layout.h", "absl/container/internal/hashtable_debug_hooks.h", "absl/strings/internal/cord_internal.h", + "absl/strings/internal/cord_rep_btree.h", + "absl/strings/internal/cord_rep_flat.h", + "absl/strings/internal/cord_rep_btree_reader.h", + "absl/strings/internal/cord_rep_btree_navigator.h", + "absl/strings/internal/cord_rep_ring.h", + "absl/strings/internal/cordz_functions.h", + "absl/strings/internal/cordz_info.h", + "absl/strings/internal/cordz_handle.h", + "absl/strings/internal/cordz_statistics.h", + "absl/strings/internal/cordz_update_tracker.h", + "absl/strings/internal/cordz_update_scope.h", + "absl/strings/internal/string_constant.h", "absl/base/internal/inline_variable.h", "absl/base/internal/cycleclock.cc", "absl/base/internal/exponential_biased.cc", @@ -45,6 +57,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/base/internal/spinlock_wait.cc", "absl/base/internal/strerror.cc", "absl/base/internal/sysinfo.cc", + "absl/base/internal/thread_annotations.h", "absl/base/internal/thread_identity.cc", "absl/base/internal/throw_delegate.cc", "absl/base/internal/unscaledcycleclock.cc", @@ -117,6 +130,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/strings/str_split.cc", "absl/strings/string_view.cc", "absl/strings/substitute.cc", + "absl/synchronization/mutex.h", "absl/synchronization/barrier.cc", "absl/synchronization/blocking_counter.cc", "absl/synchronization/internal/create_thread_identity.cc", @@ -188,6 +202,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/strings/internal/charconv_bigint.h", "absl/strings/escaping.h", "absl/status/status.h", + "absl/status/internal/status_internal.h", "absl/strings/cord.h", "absl/random/internal/randen_slow.h", "absl/random/internal/randen_detect.h", @@ -198,7 +213,14 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/random/internal/pool_urbg.h", "absl/random/internal/distribution_test_util.h", "absl/flags/usage.h", + "absl/flags/commandlineflag.h", + "absl/flags/internal/sequence_lock.h", + "absl/flags/internal/private_handle_accessor.h", + "absl/flags/reflection.h", "absl/numeric/int128.h", + "absl/numeric/bits.h", + "absl/numeric/internal/bits.h", + "absl/numeric/internal/representation.h", "absl/flags/marshalling.h", "absl/flags/parse.h", "absl/flags/internal/commandlineflag.h", @@ -216,6 +238,8 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/container/internal/raw_hash_set.h", "absl/debugging/failure_signal_handler.h", "absl/container/internal/hash_generator_testing.h", + "absl/container/flat_hash_map.h", + "absl/container/internal/hash_function_defaults.h", "absl/base/internal/spinlock_wait.h", "absl/base/log_severity.h", "absl/base/internal/sysinfo.h", @@ -231,6 +255,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/time/internal/cctz/include/cctz/zone_info_source.h", "absl/time/time.h", "absl/time/clock.h", + "absl/synchronization/internal/futex.h", "absl/synchronization/internal/waiter.h", "absl/strings/str_split.h", "absl/strings/internal/str_format/float_conversion.h", @@ -245,10 +270,13 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/hash/internal/hash.h", "absl/random/internal/nanobenchmark.h", "absl/hash/internal/city.h", + "absl/hash/internal/low_level_hash.h", "absl/debugging/symbolize.h", "absl/debugging/internal/stack_consumption.h", "absl/flags/internal/flag.h", "absl/container/internal/hashtablez_sampler.h", + "absl/container/internal/raw_hash_map.h", + "absl/profiling/internal/sample_recorder.h", "absl/base/internal/unscaledcycleclock.h", "absl/base/internal/thread_identity.h", "absl/base/internal/cycleclock.h", @@ -293,7 +321,9 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/container/internal/have_sse.h", "absl/container/internal/inlined_vector.h", "absl/debugging/internal/stacktrace_unimplemented-inl.inc", + "absl/debugging/internal/stacktrace_generic-inl.inc", "absl/debugging/symbolize_unimplemented.inc", + "absl/debugging/symbolize_darwin.inc", "absl/flags/config.h", "absl/flags/internal/path_util.h", "absl/hash/hash.h", @@ -314,6 +344,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/types/internal/variant.h", "absl/base/internal/direct_mmap.h", "absl/base/internal/spinlock_posix.inc", + "absl/base/internal/dynamic_annotations.h", "absl/container/fixed_array.h", "absl/container/internal/common.h", "absl/container/internal/compressed_tuple.h", @@ -328,6 +359,8 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [ "absl/random/internal/wide_multiply.h", "absl/container/internal/hash_policy_traits.h", "absl/functional/internal/function_ref.h", + "absl/functional/bind_front.h", + "absl/functional/internal/front_binder.h", ]] webrtc_sources = [ @@ -1658,6 +1691,7 @@ webrtc_sources = [ "call/flexfec_receive_stream.h", "call/flexfec_receive_stream_impl.h", "call/receive_time_calculator.h", + "call/receive_stream.h", "call/rtp_bitrate_configurator.h", "call/rtp_config.h", "call/rtp_demuxer.h", @@ -2297,8 +2331,11 @@ webrtc_sources = [ "pc/media_session.h", "pc/media_stream.h", "pc/media_stream_observer.h", + "pc/media_stream_track_proxy.h", + "pc/media_stream_proxy.h", "pc/peer_connection.h", "pc/peer_connection_factory.h", + "pc/proxy.h", "pc/remote_audio_source.h", "pc/rtc_stats_collector.h", "pc/rtc_stats_traversal.h", @@ -2307,6 +2344,8 @@ webrtc_sources = [ "pc/rtp_parameters_conversion.h", "pc/rtp_receiver.h", "pc/rtp_sender.h", + "pc/rtp_receiver_proxy.h", + "pc/rtp_sender_proxy.h", "pc/rtp_transceiver.h", "pc/rtp_transport.h", "pc/sctp_data_channel_transport.h", @@ -2352,6 +2391,7 @@ webrtc_sources = [ "modules/rtp_rtcp/source/byte_io.h", "modules/video_capture/video_capture.h", "modules/video_coding/codecs/h264/include/h264_globals.h", + "modules/video_coding/codecs/h264/h264_color_space.h", "modules/video_coding/codecs/interface/common_constants.h", "modules/video_coding/include/video_coding.h", "modules/video_coding/internal_defines.h", @@ -2409,6 +2449,14 @@ webrtc_sources = [ "rtc_base/system/unused.h", "rtc_base/time_utils.h", "rtc_base/units/unit_base.h", + "rtc_base/containers/flat_map.h", + "rtc_base/containers/flat_tree.h", + "rtc_base/containers/as_const.h", + "rtc_base/containers/not_fn.h", + "rtc_base/containers/invoke.h", + "rtc_base/containers/void_t.h", + "rtc_base/containers/flat_set.h", + "rtc_base/containers/identity.h", "video/adaptation/encode_usage_resource.h", "video/adaptation/overuse_frame_detector.h", "video/call_stats.h", @@ -2522,6 +2570,7 @@ webrtc_sources = [ "api/stats/rtc_stats_collector_callback.h", "api/transport/rtp/rtp_source.h", "api/video/recordable_encoded_frame.h", + "api/video/render_resolution.h", "api/video_codecs/bitstream_parser.h", "api/video_codecs/vp8_frame_buffer_controller.h", "media/base/delayable.h", @@ -2545,6 +2594,7 @@ webrtc_sources = [ "modules/audio_coding/codecs/isac/main/source/pitch_filter.h", "modules/audio_device/audio_device_config.h", "modules/audio_device/include/audio_device_default.h", + "modules/audio_device/include/audio_device_factory.h", "modules/audio_processing/aec3/delay_estimate.h", "modules/audio_processing/aec3/vector_math.h", "modules/audio_processing/agc/gain_control.h", @@ -2576,6 +2626,9 @@ webrtc_sources = [ "call/audio_sender.h", "call/rtp_transport_controller_send_interface.h", "call/rtp_video_sender_interface.h", + "call/rtp_transport_config.h", + "call/rtp_transport_controller_send_factory.h", + "call/rtp_transport_controller_send_factory_interface.h", "modules/audio_coding/audio_network_adaptor/util/threshold_curve.h", "modules/audio_coding/codecs/ilbc/cb_mem_energy.h", "modules/audio_coding/codecs/ilbc/do_plc.h", @@ -2628,6 +2681,8 @@ webrtc_sources = [ "p2p/base/dtls_transport_factory.h", "p2p/base/udp_port.h", "pc/peer_connection_internal.h", + "pc/peer_connection_factory_proxy.h", + "pc/peer_connection_proxy.h", "pc/used_ids.h", "rtc_base/numerics/divide_round.h", "rtc_base/system/thread_registry.h", @@ -2854,6 +2909,7 @@ webrtc_sources = [ "api/video_codecs/vp9_profile.cc", "api/video_codecs/h264_profile_level_id.h", "api/video_codecs/h264_profile_level_id.cc", + "api/video_track_source_proxy_factory.h", "modules/remote_bitrate_estimator/packet_arrival_map.h", "modules/remote_bitrate_estimator/packet_arrival_map.cc", "modules/audio_processing/agc/clipping_predictor.h",