Initial invite requests implementation

This commit is contained in:
Ilya Laktyushin 2021-10-06 00:03:40 +04:00
parent da5e87c515
commit 4a12dcbb22
52 changed files with 2332 additions and 336 deletions

View File

@ -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.";

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -53,6 +53,8 @@ swift_library(
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/AvatarNode:AvatarNode",
"//submodules/LocalizedPeerData:LocalizedPeerData",
],
visibility = [
"//visibility:public",

View File

@ -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
}

View File

@ -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<Void, NoError>?, (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
}
}
}

View File

@ -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

View File

@ -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<PresentationData, NoError>)? = 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
}

View File

@ -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))
}
}

View File

@ -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)

View File

@ -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<Void, NoError>?, (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)
}
}
}

View File

@ -22,6 +22,7 @@ swift_library(
"//submodules/SelectablePeerNode:SelectablePeerNode",
"//submodules/PeerInfoUI:PeerInfoUI",
"//submodules/UndoUI:UndoUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
visibility = [
"//visibility:public",

View File

@ -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 {

View File

@ -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<Void, NoError>) {
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<Void, NoError>) {
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)
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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> {

View File

@ -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<Void, NoError>?, (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))
}
})
})

View File

@ -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<CLLocation?, NoError>) -> Sign
}
}
func driveEta(coordinate: CLLocationCoordinate2D) -> Signal<Double?, NoError> {
func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal<Double?, NoError> {
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<Double?, NoError> {
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)

View File

@ -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 {

View File

@ -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<LocationPinAnnotation?>(value: nil)
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
let previousEntries = Atomic<[LocationViewEntry]?>(value: nil)
let previousHadTravelTimes = Atomic<Bool>(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

View File

@ -1,7 +1,7 @@
#import <NetworkLogging/NetworkLogging.h>
#import <Foundation/Foundation.h>
#import <MtProtoKit/MtLogging.h>
#import <MtProtoKit/MTLogging.h>
static void (*bridgingTrace)(NSString *, NSString *);
void setBridgingTraceFunction(void (*f)(NSString *, NSString *)) {

View File

@ -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)

View File

@ -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)

View File

@ -1,6 +1,6 @@
#import <ShareItemsImpl/TGItemProviderSignals.h>
#import <MTProtoKit/MTProtoKit.h>
#import <MtProtoKit/MtProtoKit.h>
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>

View File

@ -1,6 +1,6 @@
#import <ShareItemsImpl/TGShareLocationSignals.h>
#import <MTProtoKit/MTProtoKit.h>
#import <MtProtoKit/MtProtoKit.h>
NSString *const TGShareAppleMapsHost = @"maps.apple.com";
NSString *const TGShareAppleMapsPath = @"/maps";

View File

@ -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()

View File

@ -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()
}

View File

@ -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),

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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();
}

View File

@ -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<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(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 {

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
let requestsStatePromise = Promise<PeerInvitationImportersState?>(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<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
let requestsStatePromise = Promise<PeerInvitationImportersState?>(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
)
}
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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",

View File

@ -5,6 +5,7 @@ objc_library(
module_name = "ogg",
srcs = glob([
"Sources/*.c",
"Sources/*.h",
]),
hdrs = glob([
"include/ogg/*.h",

View File

@ -5,6 +5,7 @@ objc_library(
module_name = "opusfile",
srcs = glob([
"Sources/*.c",
"Sources/*.h",
]),
hdrs = glob([
"include/opusfile/*.h",

View File

@ -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",