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"; "Activity.ChoosingSticker" = "choosing sticker";
"DialogList.SingleChoosingStickerSuffix" = "%@ is choosing sticker"; "DialogList.SingleChoosingStickerSuffix" = "%@ is choosing sticker";
"Activity.TappingInteractiveEmoji" = "tapping on %@";
"WallpaperPreview.Animate" = "Animate"; "WallpaperPreview.Animate" = "Animate";
"WallpaperPreview.AnimateDescription" = "Colors will move when you send messages"; "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.JpegConversionText" = "Do you want to convert photos to JPEG?";
"MediaPicker.KeepHeic" = "Keep HEIC"; "MediaPicker.KeepHeic" = "Keep HEIC";
"MediaPicker.ConvertToJpeg" = "Convert to JPEG"; "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 var animateInputActivitiesFrame = false
let inputActivities = inputActivities?.filter({ let inputActivities = inputActivities?.filter({
switch $0.1 { switch $0.1 {
case .speakingInGroupCall, .interactingWithEmoji, .seeingEmojiInteraction: case .speakingInGroupCall, .seeingEmojiInteraction:
return false return false
default: default:
return true return true

View File

@ -60,7 +60,9 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
text = strings.DialogList_Typing text = strings.DialogList_Typing
case .choosingSticker: case .choosingSticker:
text = strings.Activity_ChoosingSticker text = strings.Activity_ChoosingSticker
case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji: case let .interactingWithEmoji(emoticon, _, _):
text = strings.Activity_TappingInteractiveEmoji(emoticon).string
case .speakingInGroupCall, .seeingEmojiInteraction:
text = "" text = ""
} }
let string = NSAttributedString(string: text, font: textFont, textColor: color) let string = NSAttributedString(string: text, font: textFont, textColor: color)
@ -80,7 +82,9 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
state = .typingText(string, lightColor) state = .typingText(string, lightColor)
case .choosingSticker: case .choosingSticker:
state = .choosingSticker(string, lightColor) state = .choosingSticker(string, lightColor)
case .seeingEmojiInteraction, .interactingWithEmoji: case .interactingWithEmoji:
state = .interactingWithEmoji(string, lightColor)
case .seeingEmojiInteraction:
state = .none state = .none
} }
} else { } else {

View File

@ -128,7 +128,7 @@ public class ChatTitleActivityContentNode: ASDisplayNode {
if case .center = alignment { if case .center = alignment {
self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0 + offset) self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0 + offset)
} else { } 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 return size
} }

View File

@ -24,6 +24,7 @@ public enum ChatTitleActivityNodeState: Equatable {
case recordingVideo(NSAttributedString, UIColor) case recordingVideo(NSAttributedString, UIColor)
case playingGame(NSAttributedString, UIColor) case playingGame(NSAttributedString, UIColor)
case choosingSticker(NSAttributedString, UIColor) case choosingSticker(NSAttributedString, UIColor)
case interactingWithEmoji(NSAttributedString, UIColor)
func contentNode() -> ChatTitleActivityContentNode? { func contentNode() -> ChatTitleActivityContentNode? {
switch self { switch self {
@ -43,6 +44,8 @@ public enum ChatTitleActivityNodeState: Equatable {
return ChatPlayingActivityContentNode(text: text, color: color) return ChatPlayingActivityContentNode(text: text, color: color)
case let .choosingSticker(text, color): case let .choosingSticker(text, color):
return ChatChoosingStickerActivityContentNode(text: text, color: 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 { private class LeftAlignedIconButton: UIButton {
override func titleRect(forContentRect contentRect: CGRect) -> CGRect { 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 imageSize = currentImage?.size ?? .zero
let availableWidth = contentRect.width - imageEdgeInsets.right - imageSize.width - titleRect.width titleRect.origin.x = imageSize.width + 2.0
return titleRect.offsetBy(dx: round(availableWidth / 2) - imageEdgeInsets.left, dy: 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/ShimmerEffect:ShimmerEffect",
"//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/AvatarNode:AvatarNode",
"//submodules/LocalizedPeerData:LocalizedPeerData",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -34,6 +34,7 @@ private final class InviteLinkEditControllerArguments {
private enum InviteLinksEditSection: Int32 { private enum InviteLinksEditSection: Int32 {
case time case time
case usage case usage
case requestApproval
case revoke case revoke
} }
@ -65,10 +66,15 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool) case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool)
case usageInfo(PresentationTheme, String) case usageInfo(PresentationTheme, String)
case requestApproval(PresentationTheme, String, Bool)
case requestApprovalInfo(PresentationTheme, String)
case revoke(PresentationTheme, String) case revoke(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .requestApproval, .requestApprovalInfo:
return InviteLinksEditSection.requestApproval.rawValue
case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo: case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo:
return InviteLinksEditSection.time.rawValue return InviteLinksEditSection.time.rawValue
case .usageHeader, .usagePicker, .usageCustomPicker, .usageInfo: case .usageHeader, .usagePicker, .usageCustomPicker, .usageInfo:
@ -80,26 +86,30 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
var stableId: Int32 { var stableId: Int32 {
switch self { switch self {
case .timeHeader: case .requestApproval:
return 0 return 0
case .timePicker: case .requestApprovalInfo:
return 1 return 1
case .timeExpiryDate: case .timeHeader:
return 2 return 2
case .timeCustomPicker: case .timePicker:
return 3 return 3
case .timeInfo: case .timeExpiryDate:
return 4 return 4
case .usageHeader: case .timeCustomPicker:
return 5 return 5
case .usagePicker: case .timeInfo:
return 6 return 6
case .usageCustomPicker: case .usageHeader:
return 7 return 7
case .usageInfo: case .usagePicker:
return 8 return 8
case .revoke: case .usageCustomPicker:
return 9 return 9
case .usageInfo:
return 10
case .revoke:
return 11
} }
} }
@ -159,6 +169,18 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .revoke(lhsTheme, lhsText):
if case let .revoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .revoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -266,6 +288,16 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
}) })
case let .usageInfo(_, text): case let .usageInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) 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): case let .revoke(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.revoke() arguments.revoke()
@ -277,8 +309,16 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, presentationData: PresentationData) -> [InviteLinksEditEntry] { private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, presentationData: PresentationData) -> [InviteLinksEditEntry] {
var entries: [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)) entries.append(.timePicker(presentationData.theme, state.time))
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) 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(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo))
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) if !state.requestApproval {
entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage)) entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage))
var customValue = false var customValue = false
if case .custom = state.usage { if case .custom = state.usage {
customValue = true 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 { if let _ = invite {
entries.append(.revoke(presentationData.theme, presentationData.strings.InviteLink_Create_Revoke)) entries.append(.revoke(presentationData.theme, presentationData.strings.InviteLink_Create_Revoke))
@ -315,6 +356,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
private struct InviteLinkEditControllerState: Equatable { private struct InviteLinkEditControllerState: Equatable {
var usage: InviteLinkUsageLimit var usage: InviteLinkUsageLimit
var time: InviteLinkTimeLimit var time: InviteLinkTimeLimit
var requestApproval = false
var pickingTimeLimit = false var pickingTimeLimit = false
var pickingUsageLimit = false var pickingUsageLimit = false
var updating = false var updating = false
@ -343,9 +385,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
timeLimit = .unlimited 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 { } 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) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -443,8 +485,10 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
} }
let usageLimit = state.usage.value let usageLimit = state.usage.value
let requestNeeded = state.requestApproval
if invite == nil { 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)) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in |> deliverOnMainQueue).start(next: { invite in
completion?(invite) 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) 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 { } 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)) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in |> deliverOnMainQueue).start(next: { invite in
completion?(invite) completion?(invite)
@ -476,7 +520,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
let previousState = previousState.swap(state) let previousState = previousState.swap(state)
var animateChanges = false 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 animateChanges = true
} }

View File

@ -9,18 +9,22 @@ import PresentationDataUtils
import AnimatedStickerNode import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import AccountContext import AccountContext
import Markdown
import TextFormat
class InviteLinkHeaderItem: ListViewItem, ItemListItem { class InviteLinkHeaderItem: ListViewItem, ItemListItem {
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let text: String let text: String
let sectionId: ItemListSectionId 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.context = context
self.theme = theme self.theme = theme
self.text = text self.text = text
self.sectionId = sectionId 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) { 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) 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) { func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -89,7 +103,10 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
let leftInset: CGFloat = 32.0 + params.leftInset let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0 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 (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) 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) { override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) 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? let mainInvite: ExportedInvitation?
var isPublic = false var isPublic = false
if let peer = peer, let address = peer.addressName, !address.isEmpty && admin == nil { 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 isPublic = true
} else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) { } else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) {
mainInvite = invite 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 { if let invite = item.invite {
let count = invite.count ?? 0 let count = invite.count ?? 0
if 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 { } else {
if let usageLimit = invite.usageLimit, count == 0 && !availability.isZero { if let usageLimit = invite.usageLimit, count == 0 && !availability.isZero {
subtitleText = item.presentationData.strings.InviteLink_PeopleCanJoin(usageLimit) 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/SelectablePeerNode:SelectablePeerNode",
"//submodules/PeerInfoUI:PeerInfoUI", "//submodules/PeerInfoUI:PeerInfoUI",
"//submodules/UndoUI:UndoUI", "//submodules/UndoUI:UndoUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -20,6 +20,8 @@ public final class JoinLinkPreviewController: ViewController {
private let context: AccountContext private let context: AccountContext
private let link: String private let link: String
private var isRequest = false
private var isGroup = false
private let navigateToPeer: (EnginePeer.Id, ChatPeekTimeout?) -> Void private let navigateToPeer: (EnginePeer.Id, ChatPeekTimeout?) -> Void
private let parentNavigationController: NavigationController? private let parentNavigationController: NavigationController?
private var resolvedState: ExternalJoiningChatState? private var resolvedState: ExternalJoiningChatState?
@ -76,9 +78,15 @@ public final class JoinLinkPreviewController: ViewController {
if let strongSelf = self { if let strongSelf = self {
strongSelf.resolvedState = result strongSelf.resolvedState = result
switch result { switch result {
case let .invite(title, photoRepresentation, participantsCount, participants): case let .invite(flags, title, about, photoRepresentation, participantsCount, participants):
let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false) if flags.requestNeeded {
strongSelf.controllerNode.setPeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants?.map({ EnginePeer($0) }) ?? [], data: data) 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): case let .alreadyJoined(peerId):
strongSelf.navigateToPeer(peerId, nil) strongSelf.navigateToPeer(peerId, nil)
strongSelf.dismiss() strongSelf.dismiss()
@ -121,10 +129,14 @@ public final class JoinLinkPreviewController: ViewController {
private func join() { private func join() {
self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] peerId in self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] peerId in
if let strongSelf = self { if let strongSelf = self {
if let peerId = peerId { if strongSelf.isRequest {
strongSelf.navigateToPeer(peerId, nil) 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))
strongSelf.dismiss() } else {
if let peerId = peerId {
strongSelf.navigateToPeer(peerId, nil)
}
} }
strongSelf.dismiss()
} }
}, error: { [weak self] error in }, error: { [weak self] error in
if let strongSelf = self { if let strongSelf = self {

View File

@ -8,6 +8,27 @@ import TelegramPresentationData
import AccountContext import AccountContext
import ShareController 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 { struct JoinLinkPreviewData {
let isGroup: Bool let isGroup: Bool
let isJoined: Bool let isJoined: Bool
@ -24,18 +45,17 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode private let wrappingScrollNode: ASScrollNode
private let cancelButtonNode: ASButtonNode
private let contentContainerNode: ASDisplayNode 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 contentNode: (ASDisplayNode & ShareContentContainerNode)?
private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)?
private var animateContentNodeOffsetFromBackgroundOffset: CGFloat? private var animateContentNodeOffsetFromBackgroundOffset: CGFloat?
private let actionsBackgroundNode: ASImageNode private let cancelButton: HighlightableButtonNode
private let actionButtonNode: ShareActionButtonNode
private let actionSeparatorNode: ASDisplayNode
var dismiss: (() -> Void)? var dismiss: (() -> Void)?
var cancel: (() -> Void)? var cancel: (() -> Void)?
@ -51,28 +71,12 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
init(context: AccountContext, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) { init(context: AccountContext, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) {
self.context = context self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.requestLayout = requestLayout 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 = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.delaysContentTouches = false
@ -81,35 +85,22 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
self.dimNode = ASDisplayNode() self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) 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 = ASDisplayNode()
self.contentContainerNode.isOpaque = false self.contentContainerNode.isOpaque = false
self.contentContainerNode.clipsToBounds = true
self.contentBackgroundNode = ASImageNode() self.backgroundNode = ASDisplayNode()
self.contentBackgroundNode.displaysAsynchronously = false self.backgroundNode.clipsToBounds = true
self.contentBackgroundNode.displayWithoutProcessing = true self.backgroundNode.cornerRadius = 16.0
self.contentBackgroundNode.image = roundedBackground
self.actionsBackgroundNode = ASImageNode() self.effectNode = ASDisplayNode(viewBlock: {
self.actionsBackgroundNode.isLayerBacked = true return UIVisualEffectView(effect: UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark))
self.actionsBackgroundNode.displayWithoutProcessing = true })
self.actionsBackgroundNode.displaysAsynchronously = false
self.actionsBackgroundNode.image = halfRoundedBackground
self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.contentBackgroundNode = ASDisplayNode()
self.actionButtonNode.displaysAsynchronously = false self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.actionButtonNode.titleNode.displaysAsynchronously = false
self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted)
self.actionSeparatorNode = ASDisplayNode() self.cancelButton = HighlightableButtonNode()
self.actionSeparatorNode.isLayerBacked = true self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.actionSeparatorNode.displaysAsynchronously = false
self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
super.init() super.init()
@ -122,25 +113,18 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
self.wrappingScrollNode.view.delegate = self self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode) self.addSubnode(self.wrappingScrollNode)
self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
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.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.actionSeparatorNode) self.wrappingScrollNode.addSubnode(self.cancelButton)
self.contentContainerNode.addSubnode(self.actionsBackgroundNode)
self.contentContainerNode.addSubnode(self.actionButtonNode)
self.transitionToContentNode(ShareLoadingContainerNode(theme: theme, forceNativeAppearance: false)) self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false))
self.actionButtonNode.alpha = 0.0
self.actionSeparatorNode.alpha = 0.0
self.actionsBackgroundNode.alpha = 0.0
self.ready.set(.single(true)) self.ready.set(.single(true))
self.didSetReady = true self.didSetReady = true
@ -230,45 +214,31 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
insets.top = max(10.0, insets.top) insets.top = max(10.0, insets.top)
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
if insets.bottom > 0 { if insets.bottom > 0.0 {
bottomInset -= 12.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 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 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 contentFrame = contentContainerFrame
let bottomGridInset = buttonHeight self.containerLayout = (layout, navigationBarHeight, 0.0)
self.containerLayout = (layout, navigationBarHeight, bottomGridInset)
self.scheduledLayoutTransitionRequest = nil self.scheduledLayoutTransitionRequest = nil
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) 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.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.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))) let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height))
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))
if let contentNode = self.contentNode { 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)) 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: bottomGridInset, transition: transition) contentNode.updateLayout(size: gridSize, bottomInset: 0.0, transition: transition)
} }
} }
@ -282,14 +252,11 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
if insets.bottom > 0 { if insets.bottom > 0 {
bottomInset -= 12.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 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)) 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) 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 { if backgroundFrame.maxY > contentFrame.maxY {
backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY
} }
if backgroundFrame.size.height < buttonHeight + 32.0 { backgroundFrame.size.height += 2000.0
backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height
backgroundFrame.size.height = buttonHeight + 32.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: backgroundFrame) 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 { if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset {
self.animateContentNodeOffsetFromBackgroundOffset = nil self.animateContentNodeOffsetFromBackgroundOffset = nil
@ -328,10 +299,6 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
self.cancel?() self.cancel?()
} }
@objc func installActionButtonPressed() {
self.join?()
}
func animateIn() { func animateIn() {
if self.contentNode != nil { if self.contentNode != nil {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) 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? { 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.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 return self.dimNode.view
} }
} }
@ -431,11 +395,6 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
} }
func transitionToProgress(signal: Signal<Void, NoError>) { 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)
self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false), fastOut: true) self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false), fastOut: true)
let timestamp = CACurrentMediaTime() let timestamp = CACurrentMediaTime()
self.disposable.set(signal.start(completed: { [weak self] in 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) { func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut) 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))
transition.updateAlpha(node: self.actionButtonNode, alpha: 1.0) contentNode.join = { [weak self] in
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 1.0) self?.join?()
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)
} }
self.transitionToContentNode(contentNode)
}
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)) 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 AccountContext
import SelectablePeerNode import SelectablePeerNode
import ShareController import ShareController
import SolidRoundedButtonNode
private let avatarFont = avatarPlaceholderFont(size: 26.0) private let avatarFont = avatarPlaceholderFont(size: 42.0)
private final class MoreNode: ASDisplayNode { private final class MoreNode: ASDisplayNode {
private let avatarNode = AvatarNode(font: Font.regular(24.0)) private let avatarNode = AvatarNode(font: Font.regular(24.0))
@ -27,61 +28,116 @@ private final class MoreNode: ASDisplayNode {
} }
final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { 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 var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let titleNode: ASTextNode private let titleNode: ASTextNode
private let countNode: ASTextNode private let countNode: ASTextNode
private let aboutNode: ASTextNode
private let descriptionNode: ASTextNode
private let peersScrollNode: ASScrollNode private let peersScrollNode: ASScrollNode
private let peerNodes: [SelectablePeerNode] private let peerNodes: [SelectablePeerNode]
private let moreNode: MoreNode? 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.avatarNode = AvatarNode(font: avatarFont)
self.titleNode = ASTextNode() self.titleNode = ASTextNode()
self.countNode = 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 = ASScrollNode()
self.peersScrollNode.view.showsHorizontalScrollIndicator = false 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) 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 if case let .invite(isGroup, _, _, memberCount, members) = content {
let node = SelectablePeerNode() self.peerNodes = members.map { peer in
node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), synchronousLoad: false) let node = SelectablePeerNode()
node.theme = itemTheme node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), synchronousLoad: false)
return node node.theme = itemTheme
} return node
}
if members.count < Int(memberCount) { if members.count < Int(memberCount) {
self.moreNode = MoreNode(count: Int(memberCount) - members.count) self.moreNode = MoreNode(count: Int(memberCount) - members.count)
} else {
self.moreNode = nil
}
self.actionButtonNode.title = isGroup ? strings.Invitation_JoinGroup : strings.Channel_JoinChannel
} else { } else {
self.peerNodes = []
self.moreNode = nil self.moreNode = nil
self.actionButtonNode.title = content.isGroup ? strings.MemberRequests_RequestToJoinGroup : strings.MemberRequests_RequestToJoinChannel
} }
super.init() 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.addSubnode(self.avatarNode)
self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor) self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor)
self.addSubnode(self.titleNode) 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) self.addSubnode(self.countNode)
let membersString: String let membersString: String
if isGroup { if content.isGroup {
if !members.isEmpty { if case let .invite(_, _, _, memberCount, members) = content, !members.isEmpty {
membersString = strings.Invitation_Members(memberCount) membersString = strings.Invitation_Members(memberCount)
} else { } else {
membersString = strings.Conversation_StatusMembers(memberCount) membersString = strings.Conversation_StatusMembers(content.memberCount)
} }
} else { } 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 { if !self.peerNodes.isEmpty {
for peerNode in peerNodes { for peerNode in peerNodes {
@ -90,6 +146,21 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
self.addSubnode(self.peersScrollNode) self.addSubnode(self.peersScrollNode)
} }
self.moreNode.flatMap(self.peersScrollNode.addSubnode) 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() { func activate() {
@ -106,19 +177,41 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
} }
func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { 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 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) 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) 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 peerSize = CGSize(width: 85.0, height: 95.0)
let peerInset: CGFloat = 10.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) 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() { func updateSelectedPeers() {

View File

@ -266,7 +266,9 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati
} }
}; };
carouselItem.allowCaptions = true carouselItem.allowCaptions = true
carouselItem.editingContext.setForcedCaption(initialCaption, entities: []) if !initialCaption.isEmpty {
carouselItem.editingContext.setForcedCaption(initialCaption, entities: [])
}
itemViews.append(carouselItem) itemViews.append(carouselItem)
let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in 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.shouldShowFileTipIfNeeded = showFileTooltip
controller.requestSearchController = presentWebSearch 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> { 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 AppBundle
import AvatarNode import AvatarNode
import LiveLocationTimerNode import LiveLocationTimerNode
import SolidRoundedButtonNode
final class LocationLiveListItem: ListViewItem { final class LocationLiveListItem: ListViewItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
@ -22,18 +23,33 @@ final class LocationLiveListItem: ListViewItem {
let context: AccountContext let context: AccountContext
let message: Message let message: Message
let distance: Double? let distance: Double?
let drivingTime: Double?
let transitTime: Double?
let walkingTime: Double?
let action: () -> Void let action: () -> Void
let longTapAction: () -> 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.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.context = context self.context = context
self.message = message self.message = message
self.distance = distance self.distance = distance
self.drivingTime = drivingTime
self.transitTime = transitTime
self.walkingTime = walkingTime
self.action = action self.action = action
self.longTapAction = longTapAction 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) { 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 let avatarNode: AvatarNode
private var timerNode: ChatMessageLiveLocationTimerNode? private var timerNode: ChatMessageLiveLocationTimerNode?
private var drivingButtonNode: SolidRoundedButtonNode?
private var transitButtonNode: SolidRoundedButtonNode?
private var walkingButtonNode: SolidRoundedButtonNode?
private var item: LocationLiveListItem? private var item: LocationLiveListItem?
private var layoutParams: ListViewItemLayoutParams? private var layoutParams: ListViewItemLayoutParams?
@ -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 (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 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()) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in return (nodeLayout, { [weak self] in
@ -217,6 +240,43 @@ final class LocationLiveListItemNode: ListViewItemNode {
strongSelf.addSubnode(subtitleNode) 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) let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
titleNode.frame = titleFrame 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.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.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)) 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) 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.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 { } else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil strongSelf.timerNode = nil
timerNode.removeFromSupernode() 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? { func stringForEstimatedDuration(strings: PresentationStrings, time: Double, format: (String) -> String) -> String? {
if eta > 0.0 && eta < 60.0 * 60.0 * 10.0 { if time > 0.0 {
let eta = max(eta, 60.0) let time = max(time, 60.0)
let minutes = Int32(eta / 60.0) % 60 let minutes = Int32(time / 60.0) % 60
let hours = Int32(eta / 3600.0) let hours = Int32(time / 3600.0)
let string: String let string: String
if hours > 1 { if hours > 1 {
@ -86,7 +86,7 @@ func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> St
} else { } else {
string = strings.Map_ETAMinutes(minutes) string = strings.Map_ETAMinutes(minutes)
} }
return strings.Map_DirectionsDriveEta(string).string return format(string)
} else { } else {
return nil 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 return Signal { subscriber in
let destinationPlacemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil) let destinationPlacemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil)
let destination = MKMapItem(placemark: destinationPlacemark) let destination = MKMapItem(placemark: destinationPlacemark)
@ -125,7 +125,7 @@ func driveEta(coordinate: CLLocationCoordinate2D) -> Signal<Double?, NoError> {
let request = MKDirections.Request() let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation() request.source = MKMapItem.forCurrentLocation()
request.destination = destination request.destination = destination
request.transportType = .automobile request.transportType = transportType
request.requestsAlternateRoutes = false request.requestsAlternateRoutes = false
let directions = MKDirections(request: request) let directions = MKDirections(request: request)

View File

@ -15,6 +15,7 @@ import OpenInExternalAppUI
import ShareController import ShareController
import DeviceAccess import DeviceAccess
import UndoUI import UndoUI
import MapKit
public class LocationViewParams { public class LocationViewParams {
let sendLiveLocation: (TelegramMediaMap) -> Void let sendLiveLocation: (TelegramMediaMap) -> Void
@ -43,7 +44,7 @@ class LocationViewInteraction {
let updateMapMode: (LocationMapMode) -> Void let updateMapMode: (LocationMapMode) -> Void
let toggleTrackingMode: () -> Void let toggleTrackingMode: () -> Void
let goToCoordinate: (CLLocationCoordinate2D) -> Void let goToCoordinate: (CLLocationCoordinate2D) -> Void
let requestDirections: () -> Void let requestDirections: (TelegramMediaMap, String?, OpenInLocationDirections) -> Void
let share: () -> Void let share: () -> Void
let setupProximityNotification: (Bool, MessageId?) -> Void let setupProximityNotification: (Bool, MessageId?) -> Void
let updateSendActionHighlight: (Bool) -> Void let updateSendActionHighlight: (Bool) -> Void
@ -52,7 +53,7 @@ class LocationViewInteraction {
let updateRightBarButton: (LocationViewRightBarButton) -> Void let updateRightBarButton: (LocationViewRightBarButton) -> Void
let present: (ViewController) -> 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.toggleMapModeSelection = toggleMapModeSelection
self.updateMapMode = updateMapMode self.updateMapMode = updateMapMode
self.toggleTrackingMode = toggleTrackingMode self.toggleTrackingMode = toggleTrackingMode
@ -161,12 +162,31 @@ public final class LocationViewController: ViewController {
state.selectedLocation = .coordinate(coordinate, false) state.selectedLocation = .coordinate(coordinate, false)
return state return state
} }
}, requestDirections: { [weak self] in }, requestDirections: { [weak self] location, peerName, directions in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if let location = getLocation(from: strongSelf.subject) { let item: OpenInItem = .location(location: location, directions: directions)
strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil) 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 }, share: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -176,7 +196,7 @@ public final class LocationViewController: ViewController {
let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: { 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(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 }, setupProximityNotification: { [weak self] reset, messageId in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -37,6 +37,7 @@ private struct LocationViewTransaction {
let deletions: [ListViewDeleteItem] let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem] let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem] let updates: [ListViewUpdateItem]
let gotTravelTimes: Bool
} }
private enum LocationViewEntryId: Hashable { private enum LocationViewEntryId: Hashable {
@ -48,7 +49,7 @@ private enum LocationViewEntryId: Hashable {
private enum LocationViewEntry: Comparable, Identifiable { private enum LocationViewEntry: Comparable, Identifiable {
case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?) case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?)
case toggleLiveLocation(PresentationTheme, String, 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 { var stableId: LocationViewEntryId {
switch self { switch self {
@ -56,7 +57,7 @@ private enum LocationViewEntry: Comparable, Identifiable {
return .info return .info
case .toggleLiveLocation: case .toggleLiveLocation:
return .toggleLiveLocation return .toggleLiveLocation
case let .liveLocation(_, _, _, message, _, _): case let .liveLocation(_, _, _, message, _, _, _, _, _):
return .liveLocation(message.stableId) return .liveLocation(message.stableId)
} }
} }
@ -75,8 +76,8 @@ private enum LocationViewEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsIndex): case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, 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 { 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 return true
} else { } else {
return false return false
@ -100,11 +101,11 @@ private enum LocationViewEntry: Comparable, Identifiable {
case .liveLocation: case .liveLocation:
return true return true
} }
case let .liveLocation(_, _, _, _, _, lhsIndex): case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex):
switch rhs { switch rhs {
case .info, .toggleLiveLocation: case .info, .toggleLiveLocation:
return false return false
case let .liveLocation(_, _, _, _, _, rhsIndex): case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} }
} }
@ -125,11 +126,14 @@ private enum LocationViewEntry: Comparable, Identifiable {
} else { } else {
distanceString = nil 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: { return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, eta: eta, action: {
interaction?.goToCoordinate(location.coordinate) interaction?.goToCoordinate(location.coordinate)
}, getDirections: { }, getDirections: {
interaction?.requestDirections() interaction?.requestDirections(location, nil, .driving)
}) })
case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout): case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout):
let beginTimeAndTimeout: (Double, Double)? let beginTimeAndTimeout: (Double, Double)?
@ -147,24 +151,40 @@ private enum LocationViewEntry: Comparable, Identifiable {
}, highlighted: { highlight in }, highlighted: { highlight in
interaction?.updateSendActionHighlight(highlight) interaction?.updateSendActionHighlight(highlight)
}) })
case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, _): case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _):
return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, action: { 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) { if let location = getLocation(from: message) {
interaction?.goToCoordinate(location.coordinate) 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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) } 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 { enum LocationViewLocation: Equatable {
@ -218,6 +238,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
var reportedAnnotationsReady = false var reportedAnnotationsReady = false
var onAnnotationsReady: (() -> Void)? 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) { init(context: AccountContext, presentationData: PresentationData, subject: Message, interaction: LocationViewInteraction, locationManager: LocationManager) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
@ -263,7 +291,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil { if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil {
eta = .single(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 { if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty {
address = .single(venueAddress) address = .single(venueAddress)
@ -304,13 +332,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
let previousUserAnnotation = Atomic<LocationPinAnnotation?>(value: nil) let previousUserAnnotation = Atomic<LocationPinAnnotation?>(value: nil)
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
let previousEntries = Atomic<[LocationViewEntry]?>(value: nil) let previousEntries = Atomic<[LocationViewEntry]?>(value: nil)
let previousHadTravelTimes = Atomic<Bool>(value: false)
let selfPeer = context.account.postbox.transaction { transaction -> Peer? in let selfPeer = context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(context.account.peerId) return transaction.getPeer(context.account.peerId)
} }
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta) 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 in |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta, travelTimes in
if let strongSelf = self, let location = getLocation(from: subject) { if let strongSelf = self, let location = getLocation(from: subject) {
var entries: [LocationViewEntry] = [] var entries: [LocationViewEntry] = []
var annotations: [LocationPinAnnotation] = [] var annotations: [LocationPinAnnotation] = []
@ -323,7 +352,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
var proximityNotificationRadius: Int32? var proximityNotificationRadius: Int32?
var index: Int = 0 var index: Int = 0
var isLocationView = false
if location.liveBroadcastingTimeout == nil { if location.liveBroadcastingTimeout == nil {
isLocationView = true
let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } 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 subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } let distance = userLocation.flatMap { subjectLocation.distance(from: $0) }
let timestamp = CACurrentMediaTime()
if message.localTags.contains(.OutgoingLiveLocation), let selfPeer = selfPeer { if message.localTags.contains(.OutgoingLiveLocation), let selfPeer = selfPeer {
userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: true, heading: location.heading) userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: true, heading: location.heading)
} else { } 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)) 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 index += 1
} }
@ -441,8 +507,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
let previousEntries = previousEntries.swap(entries) let previousEntries = previousEntries.swap(entries)
let previousState = previousState.swap(state) let previousState = previousState.swap(state)
let previousHadTravelTimes = previousHadTravelTimes.swap(!travelTimes.isEmpty)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction) let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction, gotTravelTimes: !travelTimes.isEmpty && !previousHadTravelTimes)
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: false) 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 { deinit {
self.disposable?.dispose() self.disposable?.dispose()
self.travelDisposables.dispose()
self.locationManager.manager.stopUpdatingHeading() self.locationManager.manager.stopUpdatingHeading()
} }
@ -633,17 +700,20 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)
let scrollToItem: ListViewScrollToItem? let scrollToItem: ListViewScrollToItem?
if !self.initialized, transition.insertions.count > 0 { if (!self.initialized && transition.insertions.count > 0) || transition.gotTravelTimes {
var index: Int = 0 var index: Int = 0
var offset: CGFloat = 0.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 index = 2
offset = 40.0 offset = 40.0
} else if transition.insertions.count == 2 { } else if transition.insertions.count == 2 {
index = 1 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 self.initialized = true
} else { } else {
scrollToItem = nil scrollToItem = nil

View File

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

View File

@ -54,12 +54,12 @@ public final class OpenInActionSheetController: ActionSheetController {
switch action { switch action {
case let .openUrl(url): case let .openUrl(url):
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 placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:])
let mapItem = MKMapItem(placemark: placemark) let mapItem = MKMapItem(placemark: placemark)
if withDirections { if let directions = directions {
let options = [ MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving ] let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ]
MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options) MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options)
} else { } else {
mapItem.openInMaps(launchOptions: nil) mapItem.openInMaps(launchOptions: nil)

View File

@ -8,7 +8,35 @@ import UrlEscaping
public enum OpenInItem { public enum OpenInItem {
case url(url: String) 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 { public enum OpenInApplication: Equatable {
@ -20,7 +48,7 @@ public enum OpenInApplication: Equatable {
public enum OpenInAction { public enum OpenInAction {
case none case none
case openUrl(url: String) case openUrl(url: String)
case openLocation(latitude: Double, longitude: Double, withDirections: Bool) case openLocation(latitude: Double, longitude: Double, directions: OpenInLocationDirections?)
} }
public final class OpenInOption { 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: { options.append(OpenInOption(identifier: "alook", application: .other(title: "Alook Browser", identifier: 1261944766, scheme: "alook", store: nil), action: {
return .openUrl(url: "alook://\(url)") return .openUrl(url: "alook://\(url)")
})) }))
case let .location(location, withDirections): case let .location(location, directions):
let lat = location.latitude let lat = location.latitude
let lon = location.longitude let lon = location.longitude
if !withDirections { if directions == nil {
if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "foursquare" { 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: { options.append(OpenInOption(identifier: "foursquare", application: .other(title: "Foursquare", identifier: 306934924, scheme: "foursquare", store: nil), action: {
return .openUrl(url: "foursquare://venues/\(venueId)") 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: { 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: { options.append(OpenInOption(identifier: "googleMaps", application: .other(title: "Google Maps", identifier: 585027354, scheme: "comgooglemaps-x-callback", store: nil), action: {
let coordinates = "\(lat),\(lon)" let coordinates = "\(lat),\(lon)"
if withDirections { if let directions = directions {
return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=driving&x-success=telegram://?resume=true&x-source=Telegram") 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 { } else {
if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "gplaces" { 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)") 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: { 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)") return .openUrl(url: "yandexmaps://build_route_on_map?lat_to=\(lat)&lon_to=\(lon)")
} else { } else {
return .openUrl(url: "yandexmaps://maps.yandex.ru/?pt=\(lon),\(lat)&z=16") 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)") 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: { options.append(OpenInOption(identifier: "citymapper", application: .other(title: "Citymapper", identifier: 469463298, scheme: "citymapper", store: nil), action: {
let endName: String let endName: String
let endAddress: 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: { options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: {
let coordinates = "\(lon),\(lat)" let coordinates = "\(lon),\(lat)"
if withDirections { if let _ = directions {
return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go") return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go")
} else { } else {
return .openUrl(url: "dgis://2gis.ru/geo/\(coordinates)") 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: { options.append(OpenInOption(identifier: "moovit", application: .other(title: "Moovit", identifier: 498477945, scheme: "moovit", store: nil), action: {
if withDirections { if let _ = directions {
let destName: String let destName: String
if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 { if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 {
destName = title 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: { 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)") 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: { options.append(OpenInOption(identifier: "waze", application: .other(title: "Waze", identifier: 323229106, scheme: "waze", store: nil), action: {
let url = "waze://?ll=\(lat),\(lon)" let url = "waze://?ll=\(lat),\(lon)"
if withDirections { if let _ = directions {
return .openUrl(url: url.appending("&navigate=yes")) return .openUrl(url: url.appending("&navigate=yes"))
} else { } else {
return .openUrl(url: url) return .openUrl(url: url)

View File

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

View File

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

View File

@ -23,6 +23,7 @@ public enum SolidRoundedButtonFont {
public final class SolidRoundedButtonNode: ASDisplayNode { public final class SolidRoundedButtonNode: ASDisplayNode {
private var theme: SolidRoundedButtonTheme private var theme: SolidRoundedButtonTheme
private var font: SolidRoundedButtonFont private var font: SolidRoundedButtonFont
private var fontSize: CGFloat
private let buttonBackgroundNode: ASDisplayNode private let buttonBackgroundNode: ASDisplayNode
private let buttonGlossNode: SolidRoundedButtonGlossNode 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.theme = theme
self.font = font self.font = font
self.fontSize = fontSize
self.buttonHeight = height self.buttonHeight = height
self.buttonCornerRadius = cornerRadius self.buttonCornerRadius = cornerRadius
self.title = title self.title = title
@ -128,7 +130,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
self.buttonGlossNode.color = theme.foregroundColor 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) self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
if let width = self.validLayout { if let width = self.validLayout {
@ -150,7 +152,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
transition.updateFrame(node: self.buttonNode, frame: buttonFrame) transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
if self.title != self.titleNode.attributedText?.string { 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() let iconSize = self.iconNode.image?.size ?? CGSize()

View File

@ -893,6 +893,7 @@ private final class PeerInvitationImportersContextImpl {
var results = self.results var results = self.results
results.removeAll(where: { $0.peer.peerId == peerId}) results.removeAll(where: { $0.peer.peerId == peerId})
self.results = results self.results = results
self.count = max(0, self.count - 1)
self.updateState() self.updateState()
self.updateCache() 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 defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
public let defaultPresentationTheme = makeDefaultDayPresentationTheme(serviceBackgroundColor: defaultServiceBackgroundColor, day: false, preview: false) 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 { 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) { if (theme.referenceTheme != .day && theme.referenceTheme != .dayClassic) {
@ -346,7 +346,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
let intro = PresentationThemeIntro( let intro = PresentationThemeIntro(
statusBarStyle: .black, statusBarStyle: .black,
primaryTextColor: UIColor(rgb: 0x000000), primaryTextColor: UIColor(rgb: 0x000000),
accentTextColor: UIColor(rgb: 0x007ee5), accentTextColor: defaultDayAccentColor,
disabledTextColor: UIColor(rgb: 0xd0d0d0), disabledTextColor: UIColor(rgb: 0xd0d0d0),
startButtonColor: UIColor(rgb: 0x2ca5e0), startButtonColor: UIColor(rgb: 0x2ca5e0),
dotColor: UIColor(rgb: 0xd9d9d9) dotColor: UIColor(rgb: 0xd9d9d9)
@ -358,12 +358,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
) )
let rootNavigationBar = PresentationThemeRootNavigationBar( let rootNavigationBar = PresentationThemeRootNavigationBar(
buttonColor: UIColor(rgb: 0x007ee5), buttonColor: defaultDayAccentColor,
disabledButtonColor: UIColor(rgb: 0xd0d0d0), disabledButtonColor: UIColor(rgb: 0xd0d0d0),
primaryTextColor: UIColor(rgb: 0x000000), primaryTextColor: UIColor(rgb: 0x000000),
secondaryTextColor: UIColor(rgb: 0x787878), secondaryTextColor: UIColor(rgb: 0x787878),
controlColor: UIColor(rgb: 0x7e8791), controlColor: UIColor(rgb: 0x7e8791),
accentTextColor: UIColor(rgb: 0x007ee5), accentTextColor: defaultDayAccentColor,
blurredBackgroundColor: UIColor(rgb: 0xf2f2f2, alpha: 0.9), blurredBackgroundColor: UIColor(rgb: 0xf2f2f2, alpha: 0.9),
opaqueBackgroundColor: UIColor(rgb: 0xf7f7f7).mixedWith(.white, alpha: 0.14), opaqueBackgroundColor: UIColor(rgb: 0xf7f7f7).mixedWith(.white, alpha: 0.14),
separatorColor: UIColor(rgb: 0xc8c7cc), separatorColor: UIColor(rgb: 0xc8c7cc),
@ -382,9 +382,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
backgroundColor: rootNavigationBar.blurredBackgroundColor, backgroundColor: rootNavigationBar.blurredBackgroundColor,
separatorColor: UIColor(rgb: 0xa3a3a3), separatorColor: UIColor(rgb: 0xa3a3a3),
iconColor: UIColor(rgb: 0x959595), iconColor: UIColor(rgb: 0x959595),
selectedIconColor: UIColor(rgb: 0x007ee5), selectedIconColor: defaultDayAccentColor,
textColor: UIColor(rgb: 0x959595), textColor: UIColor(rgb: 0x959595),
selectedTextColor: UIColor(rgb: 0x007ee5), selectedTextColor: defaultDayAccentColor,
badgeBackgroundColor: UIColor(rgb: 0xff3b30), badgeBackgroundColor: UIColor(rgb: 0xff3b30),
badgeStrokeColor: UIColor(rgb: 0xff3b30), badgeStrokeColor: UIColor(rgb: 0xff3b30),
badgeTextColor: UIColor(rgb: 0xffffff) badgeTextColor: UIColor(rgb: 0xffffff)
@ -392,7 +392,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
let navigationSearchBar = PresentationThemeNavigationSearchBar( let navigationSearchBar = PresentationThemeNavigationSearchBar(
backgroundColor: UIColor(rgb: 0xffffff), backgroundColor: UIColor(rgb: 0xffffff),
accentColor: UIColor(rgb: 0x007ee5), accentColor: defaultDayAccentColor,
inputFillColor: UIColor(rgb: 0x000000, alpha: 0.06), inputFillColor: UIColor(rgb: 0x000000, alpha: 0.06),
inputTextColor: UIColor(rgb: 0x000000), inputTextColor: UIColor(rgb: 0x000000),
inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93),
@ -423,7 +423,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
itemPrimaryTextColor: UIColor(rgb: 0x000000), itemPrimaryTextColor: UIColor(rgb: 0x000000),
itemSecondaryTextColor: UIColor(rgb: 0x8e8e93), itemSecondaryTextColor: UIColor(rgb: 0x8e8e93),
itemDisabledTextColor: UIColor(rgb: 0x8e8e93), itemDisabledTextColor: UIColor(rgb: 0x8e8e93),
itemAccentColor: UIColor(rgb: 0x007ee5), itemAccentColor: defaultDayAccentColor,
itemHighlightedColor: UIColor(rgb: 0x00b12c), itemHighlightedColor: UIColor(rgb: 0x00b12c),
itemDestructiveColor: UIColor(rgb: 0xff3b30), itemDestructiveColor: UIColor(rgb: 0xff3b30),
itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce),
@ -443,12 +443,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xf09a37), foregroundColor: UIColor(rgb: 0xffffff)), neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xf09a37), foregroundColor: UIColor(rgb: 0xffffff)),
destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3824), foregroundColor: UIColor(rgb: 0xffffff)), destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3824), foregroundColor: UIColor(rgb: 0xffffff)),
constructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x00c900), 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)), warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff9500), foregroundColor: UIColor(rgb: 0xffffff)),
inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: UIColor(rgb: 0xffffff)) inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: UIColor(rgb: 0xffffff))
), ),
itemCheckColors: PresentationThemeFillStrokeForeground( itemCheckColors: PresentationThemeFillStrokeForeground(
fillColor: UIColor(rgb: 0x007ee5), fillColor: defaultDayAccentColor,
strokeColor: UIColor(rgb: 0xc7c7cc), strokeColor: UIColor(rgb: 0xc7c7cc),
foregroundColor: UIColor(rgb: 0xffffff) foregroundColor: UIColor(rgb: 0xffffff)
), ),
@ -471,7 +471,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
scrollIndicatorColor: UIColor(white: 0.0, alpha: 0.3), scrollIndicatorColor: UIColor(white: 0.0, alpha: 0.3),
pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7), pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7),
inputClearButtonColor: UIColor(rgb: 0xcccccc), 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)), 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( paymentOption: PresentationThemeList.PaymentOption(
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1), inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1),
@ -495,12 +495,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
messageTextColor: UIColor(rgb: 0x8e8e93), messageTextColor: UIColor(rgb: 0x8e8e93),
messageHighlightedTextColor: UIColor(rgb: 0x000000), messageHighlightedTextColor: UIColor(rgb: 0x000000),
messageDraftTextColor: UIColor(rgb: 0xdd4b39), messageDraftTextColor: UIColor(rgb: 0xdd4b39),
checkmarkColor: day ? UIColor(rgb: 0x007ee5) : UIColor(rgb: 0x21c004), checkmarkColor: day ? defaultDayAccentColor : UIColor(rgb: 0x21c004),
pendingIndicatorColor: UIColor(rgb: 0x8e8e93), pendingIndicatorColor: UIColor(rgb: 0x8e8e93),
failedFillColor: UIColor(rgb: 0xff3b30), failedFillColor: UIColor(rgb: 0xff3b30),
failedForegroundColor: UIColor(rgb: 0xffffff), failedForegroundColor: UIColor(rgb: 0xffffff),
muteIconColor: UIColor(rgb: 0xa7a7ad), muteIconColor: UIColor(rgb: 0xa7a7ad),
unreadBadgeActiveBackgroundColor: UIColor(rgb: 0x007ee5), unreadBadgeActiveBackgroundColor: defaultDayAccentColor,
unreadBadgeActiveTextColor: UIColor(rgb: 0xffffff), unreadBadgeActiveTextColor: UIColor(rgb: 0xffffff),
unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb), unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb),
unreadBadgeInactiveTextColor: UIColor(rgb: 0xffffff), unreadBadgeInactiveTextColor: UIColor(rgb: 0xffffff),
@ -509,7 +509,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
regularSearchBarColor: UIColor(rgb: 0xe9e9e9), regularSearchBarColor: UIColor(rgb: 0xe9e9e9),
sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7),
sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), sectionHeaderTextColor: UIColor(rgb: 0x8e8e93),
verifiedIconFillColor: UIColor(rgb: 0x007ee5), verifiedIconFillColor: defaultDayAccentColor,
verifiedIconForegroundColor: UIColor(rgb: 0xffffff), verifiedIconForegroundColor: UIColor(rgb: 0xffffff),
secretIconColor: UIColor(rgb: 0x00b12c), secretIconColor: UIColor(rgb: 0x00b12c),
pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)), 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), primaryTextColor: UIColor(rgb: 0x000000),
secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6),
linkTextColor: UIColor(rgb: 0x004bad), linkTextColor: UIColor(rgb: 0x004bad),
linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3), linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3),
scamColor: UIColor(rgb: 0xff3b30), scamColor: UIColor(rgb: 0xff3b30),
textHighlightColor: UIColor(rgb: 0xffe438), textHighlightColor: UIColor(rgb: 0xffe438),
accentTextColor: UIColor(rgb: 0x007ee5), accentTextColor: defaultDayAccentColor,
accentControlColor: UIColor(rgb: 0x007ee5), accentControlColor: defaultDayAccentColor,
accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6), accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaActiveControlColor: UIColor(rgb: 0x007ee5), mediaActiveControlColor: defaultDayAccentColor,
mediaInactiveControlColor: UIColor(rgb: 0xcacaca), mediaInactiveControlColor: UIColor(rgb: 0xcacaca),
mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff),
pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6),
@ -544,15 +544,15 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
fileDescriptionColor: UIColor(rgb: 0x999999), fileDescriptionColor: UIColor(rgb: 0x999999),
fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaPlaceholderColor: UIColor(rgb: 0xe8ecf0), 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), 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( 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)), 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), primaryTextColor: UIColor(rgb: 0x000000),
secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8),
linkTextColor: UIColor(rgb: 0x004bad), linkTextColor: UIColor(rgb: 0x004bad),
linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3), linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3),
scamColor: UIColor(rgb: 0xff3b30), scamColor: UIColor(rgb: 0xff3b30),
textHighlightColor: UIColor(rgb: 0xffe438), textHighlightColor: UIColor(rgb: 0xffe438),
accentTextColor: UIColor(rgb: 0x00a700), accentTextColor: UIColor(rgb: 0x00a700),
@ -582,7 +582,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear),
shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)),
mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: 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)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)),
mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6),
stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.25)), stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.25)),
@ -595,28 +595,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
primaryTextColor: UIColor(rgb: 0x000000), primaryTextColor: UIColor(rgb: 0x000000),
secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6),
linkTextColor: UIColor(rgb: 0x004bad), linkTextColor: UIColor(rgb: 0x004bad),
linkHighlightColor: UIColor(rgb: 0x007ee5, alpha: 0.3), linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3),
scamColor: UIColor(rgb: 0xff3b30), scamColor: UIColor(rgb: 0xff3b30),
textHighlightColor: UIColor(rgb: 0xffc738), textHighlightColor: UIColor(rgb: 0xffc738),
accentTextColor: UIColor(rgb: 0x007ee5), accentTextColor: defaultDayAccentColor,
accentControlColor: UIColor(rgb: 0x007ee5), accentControlColor: defaultDayAccentColor,
accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6), accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaActiveControlColor: UIColor(rgb: 0x007ee5), mediaActiveControlColor: defaultDayAccentColor,
mediaInactiveControlColor: UIColor(rgb: 0xcacaca), mediaInactiveControlColor: UIColor(rgb: 0xcacaca),
mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff),
pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6),
fileTitleColor: UIColor(rgb: 0x007ee5), fileTitleColor: defaultDayAccentColor,
fileDescriptionColor: UIColor(rgb: 0x999999), fileDescriptionColor: UIColor(rgb: 0x999999),
fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.95), 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)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)), actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: defaultDayAccentColor),
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor),
textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.3), textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.3),
textSelectionKnobColor: UIColor(rgb: 0x007ee5)), textSelectionKnobColor: defaultDayAccentColor),
outgoing: PresentationThemePartedColors( 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), primaryTextColor: UIColor(rgb: 0xffffff),
secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65),
linkTextColor: UIColor(rgb: 0xffffff), linkTextColor: UIColor(rgb: 0xffffff),
@ -636,8 +636,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
mediaPlaceholderColor: UIColor(rgb: 0x0077d9), 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)), 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)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)), actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: defaultDayAccentColor),
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor),
textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2),
textSelectionKnobColor: UIColor(rgb: 0xffffff)), 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)), 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), mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff),
shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0xe5e5ea)), 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)), 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)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)),
mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6), mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6),
stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor.withAlphaComponent(0.3), withoutWallpaper: UIColor(rgb: 0xf7f7f7)), stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor.withAlphaComponent(0.3), withoutWallpaper: UIColor(rgb: 0xf7f7f7)),
@ -674,8 +674,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
) )
let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl( let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl(
buttonColor: UIColor(rgb: 0x007ee5), buttonColor: defaultDayAccentColor,
micLevelColor: UIColor(rgb: 0x007ee5, alpha: 0.2), micLevelColor: defaultDayAccentColor.withAlphaComponent(0.2),
activeIconColor: UIColor(rgb: 0xffffff) activeIconColor: UIColor(rgb: 0xffffff)
) )
@ -683,7 +683,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
panelBackgroundColor: rootNavigationBar.blurredBackgroundColor, panelBackgroundColor: rootNavigationBar.blurredBackgroundColor,
panelBackgroundColorNoWallpaper: rootNavigationBar.blurredBackgroundColor, panelBackgroundColorNoWallpaper: rootNavigationBar.blurredBackgroundColor,
panelSeparatorColor: UIColor(rgb: 0xb2b2b2), panelSeparatorColor: UIColor(rgb: 0xb2b2b2),
panelControlAccentColor: UIColor(rgb: 0x007ee5), panelControlAccentColor: defaultDayAccentColor,
panelControlColor: UIColor(rgb: 0x858e99), panelControlColor: UIColor(rgb: 0x858e99),
panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5), panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5),
panelControlDestructiveColor: UIColor(rgb: 0xff3b30), panelControlDestructiveColor: UIColor(rgb: 0xff3b30),
@ -692,7 +692,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
inputPlaceholderColor: UIColor(rgb: 0xbebec0), inputPlaceholderColor: UIColor(rgb: 0xbebec0),
inputTextColor: UIColor(rgb: 0x000000), inputTextColor: UIColor(rgb: 0x000000),
inputControlColor: UIColor(rgb: 0xa0a7b0), inputControlColor: UIColor(rgb: 0xa0a7b0),
actionControlFillColor: UIColor(rgb: 0x007ee5), actionControlFillColor: defaultDayAccentColor,
actionControlForegroundColor: UIColor(rgb: 0xffffff), actionControlForegroundColor: UIColor(rgb: 0xffffff),
primaryTextColor: UIColor(rgb: 0x000000), primaryTextColor: UIColor(rgb: 0x000000),
secondaryTextColor: UIColor(rgb: 0x8e8e93), secondaryTextColor: UIColor(rgb: 0x8e8e93),
@ -727,8 +727,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
fillColor: UIColor(rgb: 0xf7f7f7), fillColor: UIColor(rgb: 0xf7f7f7),
strokeColor: UIColor(rgb: 0xc8c7cc), strokeColor: UIColor(rgb: 0xc8c7cc),
foregroundColor: UIColor(rgb: 0x88888d), foregroundColor: UIColor(rgb: 0x88888d),
badgeBackgroundColor: UIColor(rgb: 0x007ee5), badgeBackgroundColor: defaultDayAccentColor,
badgeStrokeColor: UIColor(rgb: 0x007ee5), badgeStrokeColor: defaultDayAccentColor,
badgeTextColor: UIColor(rgb: 0xffffff) badgeTextColor: UIColor(rgb: 0xffffff)
) )
@ -753,12 +753,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0), opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0),
itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7), itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7),
opaqueItemSeparatorColor: UIColor(white: 0.9, alpha: 1.0), opaqueItemSeparatorColor: UIColor(white: 0.9, alpha: 1.0),
standardActionTextColor: UIColor(rgb: 0x007ee5), standardActionTextColor: defaultDayAccentColor,
destructiveActionTextColor: UIColor(rgb: 0xff3b30), destructiveActionTextColor: UIColor(rgb: 0xff3b30),
disabledActionTextColor: UIColor(rgb: 0xb3b3b3), disabledActionTextColor: UIColor(rgb: 0xb3b3b3),
primaryTextColor: UIColor(rgb: 0x000000), primaryTextColor: UIColor(rgb: 0x000000),
secondaryTextColor: UIColor(rgb: 0x5e5e5e), secondaryTextColor: UIColor(rgb: 0x8e8e93),
controlAccentColor: UIColor(rgb: 0x007ee5), controlAccentColor: defaultDayAccentColor,
inputBackgroundColor: UIColor(rgb: 0xe9e9e9), inputBackgroundColor: UIColor(rgb: 0xe9e9e9),
inputHollowBackgroundColor: UIColor(rgb: 0xffffff), inputHollowBackgroundColor: UIColor(rgb: 0xffffff),
inputBorderColor: UIColor(rgb: 0xe4e4e6), inputBorderColor: UIColor(rgb: 0xe4e4e6),
@ -778,7 +778,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
primaryColor: UIColor(rgb: 0x000000), primaryColor: UIColor(rgb: 0x000000),
secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8), secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8),
destructiveColor: UIColor(rgb: 0xff3b30), destructiveColor: UIColor(rgb: 0xff3b30),
badgeFillColor: UIColor(rgb: 0x007ee5), badgeFillColor: defaultDayAccentColor,
badgeForegroundColor: UIColor(rgb: 0xffffff), badgeForegroundColor: UIColor(rgb: 0xffffff),
badgeInactiveFillColor: UIColor(rgb: 0xb6b6bb), badgeInactiveFillColor: UIColor(rgb: 0xb6b6bb),
badgeInactiveForegroundColor: UIColor(rgb: 0xffffff), 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 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) { 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 let _ = ChatControllerCount.modify { value in
return value + 1 return value + 1
@ -4160,6 +4163,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.addMemberDisposable.dispose() self.addMemberDisposable.dispose()
self.importStateDisposable?.dispose() self.importStateDisposable?.dispose()
self.nextChannelToReadDisposable?.dispose() self.nextChannelToReadDisposable?.dispose()
self.inviteRequestsDisposable.dispose()
} }
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
@ -4729,6 +4733,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var callsPrivate: Bool = false var callsPrivate: Bool = false
var slowmodeState: ChatSlowmodeState? var slowmodeState: ChatSlowmodeState?
var activeGroupCallInfo: ChatActiveGroupCallInfo? var activeGroupCallInfo: ChatActiveGroupCallInfo?
var inviteRequestsPending: Int32?
if let cachedData = cachedData as? CachedChannelData { if let cachedData = cachedData as? CachedChannelData {
pinnedMessageId = cachedData.pinnedMessageId pinnedMessageId = cachedData.pinnedMessageId
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout { 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 { if let activeCall = cachedData.activeCall {
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
} }
inviteRequestsPending = cachedData.inviteRequestsPending
} else if let cachedData = cachedData as? CachedUserData { } else if let cachedData = cachedData as? CachedUserData {
peerIsBlocked = cachedData.isBlocked peerIsBlocked = cachedData.isBlocked
callsAvailable = cachedData.voiceCallsAvailable callsAvailable = cachedData.voiceCallsAvailable
@ -4751,6 +4757,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let activeCall = cachedData.activeCall { if let activeCall = cachedData.activeCall {
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
} }
inviteRequestsPending = cachedData.inviteRequestsPending
} else if let _ = cachedData as? CachedSecretChatData { } else if let _ = cachedData as? CachedSecretChatData {
} }
@ -4783,6 +4790,62 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate 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 { 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 strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
return state return state
@ -7269,6 +7332,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return $0.updatedShowCommands(f($0.showCommands)) 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())) }, 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 { do {

View File

@ -40,7 +40,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
break loop break loop
} }
} }
case .chatInfo, .requestInProgress, .toastAlert: case .chatInfo, .requestInProgress, .toastAlert, .inviteRequests:
selectedContext = context selectedContext = context
break loop break loop
} }
@ -119,6 +119,16 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
return panel 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 presentInviteMembers: () -> Void
let presentGigagroupHelp: () -> Void let presentGigagroupHelp: () -> Void
let updateShowCommands: ((Bool) -> Bool) -> Void let updateShowCommands: ((Bool) -> Bool) -> Void
let openInviteRequests: () -> Void
let statuses: ChatPanelInterfaceInteractionStatuses? let statuses: ChatPanelInterfaceInteractionStatuses?
init( init(
@ -218,6 +219,7 @@ final class ChatPanelInterfaceInteraction {
presentGigagroupHelp: @escaping () -> Void, presentGigagroupHelp: @escaping () -> Void,
editMessageMedia: @escaping (MessageId, Bool) -> Void, editMessageMedia: @escaping (MessageId, Bool) -> Void,
updateShowCommands: @escaping ((Bool) -> Bool) -> Void, updateShowCommands: @escaping ((Bool) -> Bool) -> Void,
openInviteRequests: @escaping () -> Void,
statuses: ChatPanelInterfaceInteractionStatuses? statuses: ChatPanelInterfaceInteractionStatuses?
) { ) {
self.setupReplyMessage = setupReplyMessage self.setupReplyMessage = setupReplyMessage
@ -303,6 +305,7 @@ final class ChatPanelInterfaceInteraction {
self.presentInviteMembers = presentInviteMembers self.presentInviteMembers = presentInviteMembers
self.presentGigagroupHelp = presentGigagroupHelp self.presentGigagroupHelp = presentGigagroupHelp
self.updateShowCommands = updateShowCommands self.updateShowCommands = updateShowCommands
self.openInviteRequests = openInviteRequests
self.statuses = statuses self.statuses = statuses
} }
} }

View File

@ -82,6 +82,7 @@ enum ChatTitlePanelContext: Equatable, Comparable {
case chatInfo case chatInfo
case requestInProgress case requestInProgress
case toastAlert(String) case toastAlert(String)
case inviteRequests([EnginePeer], Int32)
private var index: Int { private var index: Int {
switch self { switch self {
@ -93,6 +94,8 @@ enum ChatTitlePanelContext: Equatable, Comparable {
return 2 return 2
case .toastAlert: case .toastAlert:
return 3 return 3
case .inviteRequests:
return 4
} }
} }

View File

@ -144,7 +144,9 @@ final class ChatRecentActionsController: TelegramBaseController {
}, presentInviteMembers: { }, presentInviteMembers: {
}, presentGigagroupHelp: { }, presentGigagroupHelp: {
}, editMessageMedia: { _, _ in }, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in }, statuses: nil) }, updateShowCommands: { _ in
}, openInviteRequests: {
}, statuses: nil)
self.navigationItem.titleView = self.titleView self.navigationItem.titleView = self.titleView

View File

@ -181,6 +181,7 @@ final class PeerInfoScreenData {
let encryptionKeyFingerprint: SecretChatKeyFingerprint? let encryptionKeyFingerprint: SecretChatKeyFingerprint?
let globalSettings: TelegramGlobalSettings? let globalSettings: TelegramGlobalSettings?
let invitations: PeerExportedInvitationsState? let invitations: PeerExportedInvitationsState?
let requests: PeerInvitationImportersState?
init( init(
peer: Peer?, peer: Peer?,
@ -195,7 +196,8 @@ final class PeerInfoScreenData {
members: PeerInfoMembersData?, members: PeerInfoMembersData?,
encryptionKeyFingerprint: SecretChatKeyFingerprint?, encryptionKeyFingerprint: SecretChatKeyFingerprint?,
globalSettings: TelegramGlobalSettings?, globalSettings: TelegramGlobalSettings?,
invitations: PeerExportedInvitationsState? invitations: PeerExportedInvitationsState?,
requests: PeerInvitationImportersState?
) { ) {
self.peer = peer self.peer = peer
self.cachedData = cachedData self.cachedData = cachedData
@ -210,6 +212,7 @@ final class PeerInfoScreenData {
self.encryptionKeyFingerprint = encryptionKeyFingerprint self.encryptionKeyFingerprint = encryptionKeyFingerprint
self.globalSettings = globalSettings self.globalSettings = globalSettings
self.invitations = invitations self.invitations = invitations
self.requests = requests
} }
} }
@ -433,7 +436,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
members: nil, members: nil,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: globalSettings, globalSettings: globalSettings,
invitations: nil invitations: nil,
requests: nil
) )
} }
} }
@ -456,7 +460,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
members: nil, members: nil,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: nil, globalSettings: nil,
invitations: nil invitations: nil,
requests: nil
)) ))
case let .user(userPeerId, secretChatId, kind): case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext? let groupsInCommon: GroupsInCommonContext?
@ -596,7 +601,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
members: nil, members: nil,
encryptionKeyFingerprint: encryptionKeyFingerprint, encryptionKeyFingerprint: encryptionKeyFingerprint,
globalSettings: nil, globalSettings: nil,
invitations: nil invitations: nil,
requests: nil
) )
} }
case .channel: case .channel:
@ -620,15 +626,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil) let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil) let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
let requestsStatePromise = Promise<PeerInvitationImportersState?>(nil)
return combineLatest( return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true), context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId), peerInfoAvailableMediaPanes(context: context, peerId: peerId),
context.account.postbox.combinedView(keys: combinedKeys), context.account.postbox.combinedView(keys: combinedKeys),
status, status,
invitationsContextPromise.get(), 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 var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
@ -641,11 +652,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
discussionPeer = peer 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 { 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 { if canManageInvitations {
let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true)
invitationsContextPromise.set(.single(invitationsContext)) invitationsContextPromise.set(.single(invitationsContext))
@ -653,6 +664,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
} }
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( return PeerInfoScreenData(
peer: peerView.peers[peerId], peer: peerView.peers[peerId],
cachedData: peerView.cachedData, cachedData: peerView.cachedData,
@ -666,7 +685,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
members: nil, members: nil,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: nil, globalSettings: nil,
invitations: invitations invitations: invitations,
requests: requests
) )
} }
case let .group(groupId): case let .group(groupId):
@ -767,6 +787,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil) let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil) let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
let requestsStatePromise = Promise<PeerInvitationImportersState?>(nil)
return combineLatest(queue: .mainQueue(), return combineLatest(queue: .mainQueue(),
context.account.viewTracker.peerView(groupId, updateData: true), context.account.viewTracker.peerView(groupId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: groupId), peerInfoAvailableMediaPanes(context: context, peerId: groupId),
@ -774,9 +797,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
status, status,
membersData, membersData,
invitationsContextPromise.get(), 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 var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
@ -798,17 +823,17 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
} }
if currentInvitationsContext == nil { var canManageInvitations = false
var canManageInvitations = false if let group = peerViewMainPeer(peerView) as? TelegramGroup {
if let group = peerViewMainPeer(peerView) as? TelegramGroup { if case .creator = group.role {
if case .creator = group.role { canManageInvitations = true
canManageInvitations = true } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) {
} 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 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 { if canManageInvitations {
let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true)
invitationsContextPromise.set(.single(invitationsContext)) invitationsContextPromise.set(.single(invitationsContext))
@ -816,6 +841,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
} }
} }
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( return PeerInfoScreenData(
peer: peerView.peers[groupId], peer: peerView.peers[groupId],
cachedData: peerView.cachedData, cachedData: peerView.cachedData,
@ -829,7 +862,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
members: membersData, members: membersData,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: nil, globalSettings: nil,
invitations: invitations invitations: invitations,
requests: requests
) )
} }
} }

View File

@ -454,7 +454,9 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, presentInviteMembers: { }, presentInviteMembers: {
}, presentGigagroupHelp: { }, presentGigagroupHelp: {
}, editMessageMedia: { _, _ in }, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in }, statuses: nil) }, updateShowCommands: { _ in
}, openInviteRequests: {
}, statuses: nil)
self.selectionPanel.interfaceInteraction = interfaceInteraction self.selectionPanel.interfaceInteraction = interfaceInteraction
@ -494,6 +496,7 @@ private enum PeerInfoParticipantsSection {
case members case members
case admins case admins
case banned case banned
case memberRequests
} }
private enum PeerInfoMemberAction { private enum PeerInfoMemberAction {
@ -1034,7 +1037,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let ItemAbout = 2 let ItemAbout = 2
let ItemAdmins = 3 let ItemAdmins = 3
let ItemMembers = 4 let ItemMembers = 4
let ItemBanned = 5 let ItemMemberRequests = 5
let ItemBanned = 6
let ItemLocationHeader = 7 let ItemLocationHeader = 7
let ItemLocation = 8 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: { 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) 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: { 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) interaction.openParticipantsSection(.banned)
})) }))
@ -4711,6 +4722,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} }
case .banned: case .banned:
self.controller?.push(channelBlacklistController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) 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: { }, presentInviteMembers: {
}, presentGigagroupHelp: { }, presentGigagroupHelp: {
}, editMessageMedia: { _, _ in }, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in }, statuses: nil) }, updateShowCommands: { _ in
}, openInviteRequests: {
}, statuses: nil)
self.readyValue.set(self.chatListNode.ready) self.readyValue.set(self.chatListNode.ready)
} }

View File

@ -8,6 +8,7 @@ objc_library(
"Sources/**/*.mm", "Sources/**/*.mm",
"Sources/**/*.h", "Sources/**/*.h",
"tgcalls/tgcalls/**/*.h", "tgcalls/tgcalls/**/*.h",
"tgcalls/tgcalls/**/*.hpp",
"tgcalls/tgcalls/**/*.cpp", "tgcalls/tgcalls/**/*.cpp",
"tgcalls/tgcalls/**/*.mm", "tgcalls/tgcalls/**/*.mm",
"tgcalls/tgcalls/**/*.m", "tgcalls/tgcalls/**/*.m",

View File

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

View File

@ -5,6 +5,7 @@ objc_library(
module_name = "opusfile", module_name = "opusfile",
srcs = glob([ srcs = glob([
"Sources/*.c", "Sources/*.c",
"Sources/*.h",
]), ]),
hdrs = glob([ hdrs = glob([
"include/opusfile/*.h", "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/layout.h",
"absl/container/internal/hashtable_debug_hooks.h", "absl/container/internal/hashtable_debug_hooks.h",
"absl/strings/internal/cord_internal.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/inline_variable.h",
"absl/base/internal/cycleclock.cc", "absl/base/internal/cycleclock.cc",
"absl/base/internal/exponential_biased.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/spinlock_wait.cc",
"absl/base/internal/strerror.cc", "absl/base/internal/strerror.cc",
"absl/base/internal/sysinfo.cc", "absl/base/internal/sysinfo.cc",
"absl/base/internal/thread_annotations.h",
"absl/base/internal/thread_identity.cc", "absl/base/internal/thread_identity.cc",
"absl/base/internal/throw_delegate.cc", "absl/base/internal/throw_delegate.cc",
"absl/base/internal/unscaledcycleclock.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/str_split.cc",
"absl/strings/string_view.cc", "absl/strings/string_view.cc",
"absl/strings/substitute.cc", "absl/strings/substitute.cc",
"absl/synchronization/mutex.h",
"absl/synchronization/barrier.cc", "absl/synchronization/barrier.cc",
"absl/synchronization/blocking_counter.cc", "absl/synchronization/blocking_counter.cc",
"absl/synchronization/internal/create_thread_identity.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/internal/charconv_bigint.h",
"absl/strings/escaping.h", "absl/strings/escaping.h",
"absl/status/status.h", "absl/status/status.h",
"absl/status/internal/status_internal.h",
"absl/strings/cord.h", "absl/strings/cord.h",
"absl/random/internal/randen_slow.h", "absl/random/internal/randen_slow.h",
"absl/random/internal/randen_detect.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/pool_urbg.h",
"absl/random/internal/distribution_test_util.h", "absl/random/internal/distribution_test_util.h",
"absl/flags/usage.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/int128.h",
"absl/numeric/bits.h",
"absl/numeric/internal/bits.h",
"absl/numeric/internal/representation.h",
"absl/flags/marshalling.h", "absl/flags/marshalling.h",
"absl/flags/parse.h", "absl/flags/parse.h",
"absl/flags/internal/commandlineflag.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/container/internal/raw_hash_set.h",
"absl/debugging/failure_signal_handler.h", "absl/debugging/failure_signal_handler.h",
"absl/container/internal/hash_generator_testing.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/internal/spinlock_wait.h",
"absl/base/log_severity.h", "absl/base/log_severity.h",
"absl/base/internal/sysinfo.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/internal/cctz/include/cctz/zone_info_source.h",
"absl/time/time.h", "absl/time/time.h",
"absl/time/clock.h", "absl/time/clock.h",
"absl/synchronization/internal/futex.h",
"absl/synchronization/internal/waiter.h", "absl/synchronization/internal/waiter.h",
"absl/strings/str_split.h", "absl/strings/str_split.h",
"absl/strings/internal/str_format/float_conversion.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/hash/internal/hash.h",
"absl/random/internal/nanobenchmark.h", "absl/random/internal/nanobenchmark.h",
"absl/hash/internal/city.h", "absl/hash/internal/city.h",
"absl/hash/internal/low_level_hash.h",
"absl/debugging/symbolize.h", "absl/debugging/symbolize.h",
"absl/debugging/internal/stack_consumption.h", "absl/debugging/internal/stack_consumption.h",
"absl/flags/internal/flag.h", "absl/flags/internal/flag.h",
"absl/container/internal/hashtablez_sampler.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/unscaledcycleclock.h",
"absl/base/internal/thread_identity.h", "absl/base/internal/thread_identity.h",
"absl/base/internal/cycleclock.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/have_sse.h",
"absl/container/internal/inlined_vector.h", "absl/container/internal/inlined_vector.h",
"absl/debugging/internal/stacktrace_unimplemented-inl.inc", "absl/debugging/internal/stacktrace_unimplemented-inl.inc",
"absl/debugging/internal/stacktrace_generic-inl.inc",
"absl/debugging/symbolize_unimplemented.inc", "absl/debugging/symbolize_unimplemented.inc",
"absl/debugging/symbolize_darwin.inc",
"absl/flags/config.h", "absl/flags/config.h",
"absl/flags/internal/path_util.h", "absl/flags/internal/path_util.h",
"absl/hash/hash.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/types/internal/variant.h",
"absl/base/internal/direct_mmap.h", "absl/base/internal/direct_mmap.h",
"absl/base/internal/spinlock_posix.inc", "absl/base/internal/spinlock_posix.inc",
"absl/base/internal/dynamic_annotations.h",
"absl/container/fixed_array.h", "absl/container/fixed_array.h",
"absl/container/internal/common.h", "absl/container/internal/common.h",
"absl/container/internal/compressed_tuple.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/random/internal/wide_multiply.h",
"absl/container/internal/hash_policy_traits.h", "absl/container/internal/hash_policy_traits.h",
"absl/functional/internal/function_ref.h", "absl/functional/internal/function_ref.h",
"absl/functional/bind_front.h",
"absl/functional/internal/front_binder.h",
]] ]]
webrtc_sources = [ webrtc_sources = [
@ -1658,6 +1691,7 @@ webrtc_sources = [
"call/flexfec_receive_stream.h", "call/flexfec_receive_stream.h",
"call/flexfec_receive_stream_impl.h", "call/flexfec_receive_stream_impl.h",
"call/receive_time_calculator.h", "call/receive_time_calculator.h",
"call/receive_stream.h",
"call/rtp_bitrate_configurator.h", "call/rtp_bitrate_configurator.h",
"call/rtp_config.h", "call/rtp_config.h",
"call/rtp_demuxer.h", "call/rtp_demuxer.h",
@ -2297,8 +2331,11 @@ webrtc_sources = [
"pc/media_session.h", "pc/media_session.h",
"pc/media_stream.h", "pc/media_stream.h",
"pc/media_stream_observer.h", "pc/media_stream_observer.h",
"pc/media_stream_track_proxy.h",
"pc/media_stream_proxy.h",
"pc/peer_connection.h", "pc/peer_connection.h",
"pc/peer_connection_factory.h", "pc/peer_connection_factory.h",
"pc/proxy.h",
"pc/remote_audio_source.h", "pc/remote_audio_source.h",
"pc/rtc_stats_collector.h", "pc/rtc_stats_collector.h",
"pc/rtc_stats_traversal.h", "pc/rtc_stats_traversal.h",
@ -2307,6 +2344,8 @@ webrtc_sources = [
"pc/rtp_parameters_conversion.h", "pc/rtp_parameters_conversion.h",
"pc/rtp_receiver.h", "pc/rtp_receiver.h",
"pc/rtp_sender.h", "pc/rtp_sender.h",
"pc/rtp_receiver_proxy.h",
"pc/rtp_sender_proxy.h",
"pc/rtp_transceiver.h", "pc/rtp_transceiver.h",
"pc/rtp_transport.h", "pc/rtp_transport.h",
"pc/sctp_data_channel_transport.h", "pc/sctp_data_channel_transport.h",
@ -2352,6 +2391,7 @@ webrtc_sources = [
"modules/rtp_rtcp/source/byte_io.h", "modules/rtp_rtcp/source/byte_io.h",
"modules/video_capture/video_capture.h", "modules/video_capture/video_capture.h",
"modules/video_coding/codecs/h264/include/h264_globals.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/codecs/interface/common_constants.h",
"modules/video_coding/include/video_coding.h", "modules/video_coding/include/video_coding.h",
"modules/video_coding/internal_defines.h", "modules/video_coding/internal_defines.h",
@ -2409,6 +2449,14 @@ webrtc_sources = [
"rtc_base/system/unused.h", "rtc_base/system/unused.h",
"rtc_base/time_utils.h", "rtc_base/time_utils.h",
"rtc_base/units/unit_base.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/encode_usage_resource.h",
"video/adaptation/overuse_frame_detector.h", "video/adaptation/overuse_frame_detector.h",
"video/call_stats.h", "video/call_stats.h",
@ -2522,6 +2570,7 @@ webrtc_sources = [
"api/stats/rtc_stats_collector_callback.h", "api/stats/rtc_stats_collector_callback.h",
"api/transport/rtp/rtp_source.h", "api/transport/rtp/rtp_source.h",
"api/video/recordable_encoded_frame.h", "api/video/recordable_encoded_frame.h",
"api/video/render_resolution.h",
"api/video_codecs/bitstream_parser.h", "api/video_codecs/bitstream_parser.h",
"api/video_codecs/vp8_frame_buffer_controller.h", "api/video_codecs/vp8_frame_buffer_controller.h",
"media/base/delayable.h", "media/base/delayable.h",
@ -2545,6 +2594,7 @@ webrtc_sources = [
"modules/audio_coding/codecs/isac/main/source/pitch_filter.h", "modules/audio_coding/codecs/isac/main/source/pitch_filter.h",
"modules/audio_device/audio_device_config.h", "modules/audio_device/audio_device_config.h",
"modules/audio_device/include/audio_device_default.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/delay_estimate.h",
"modules/audio_processing/aec3/vector_math.h", "modules/audio_processing/aec3/vector_math.h",
"modules/audio_processing/agc/gain_control.h", "modules/audio_processing/agc/gain_control.h",
@ -2576,6 +2626,9 @@ webrtc_sources = [
"call/audio_sender.h", "call/audio_sender.h",
"call/rtp_transport_controller_send_interface.h", "call/rtp_transport_controller_send_interface.h",
"call/rtp_video_sender_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/audio_network_adaptor/util/threshold_curve.h",
"modules/audio_coding/codecs/ilbc/cb_mem_energy.h", "modules/audio_coding/codecs/ilbc/cb_mem_energy.h",
"modules/audio_coding/codecs/ilbc/do_plc.h", "modules/audio_coding/codecs/ilbc/do_plc.h",
@ -2628,6 +2681,8 @@ webrtc_sources = [
"p2p/base/dtls_transport_factory.h", "p2p/base/dtls_transport_factory.h",
"p2p/base/udp_port.h", "p2p/base/udp_port.h",
"pc/peer_connection_internal.h", "pc/peer_connection_internal.h",
"pc/peer_connection_factory_proxy.h",
"pc/peer_connection_proxy.h",
"pc/used_ids.h", "pc/used_ids.h",
"rtc_base/numerics/divide_round.h", "rtc_base/numerics/divide_round.h",
"rtc_base/system/thread_registry.h", "rtc_base/system/thread_registry.h",
@ -2854,6 +2909,7 @@ webrtc_sources = [
"api/video_codecs/vp9_profile.cc", "api/video_codecs/vp9_profile.cc",
"api/video_codecs/h264_profile_level_id.h", "api/video_codecs/h264_profile_level_id.h",
"api/video_codecs/h264_profile_level_id.cc", "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.h",
"modules/remote_bitrate_estimator/packet_arrival_map.cc", "modules/remote_bitrate_estimator/packet_arrival_map.cc",
"modules/audio_processing/agc/clipping_predictor.h", "modules/audio_processing/agc/clipping_predictor.h",