mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Initial invite requests implementation
This commit is contained in:
parent
da5e87c515
commit
4a12dcbb22
@ -6706,6 +6706,8 @@ Sorry for the inconvenience.";
|
||||
"Activity.ChoosingSticker" = "choosing sticker";
|
||||
"DialogList.SingleChoosingStickerSuffix" = "%@ is choosing sticker";
|
||||
|
||||
"Activity.TappingInteractiveEmoji" = "tapping on %@";
|
||||
|
||||
"WallpaperPreview.Animate" = "Animate";
|
||||
"WallpaperPreview.AnimateDescription" = "Colors will move when you send messages";
|
||||
|
||||
@ -6866,3 +6868,54 @@ Ads should no longer be synonymous with abuse of user privacy. Let us redefine h
|
||||
"MediaPicker.JpegConversionText" = "Do you want to convert photos to JPEG?";
|
||||
"MediaPicker.KeepHeic" = "Keep HEIC";
|
||||
"MediaPicker.ConvertToJpeg" = "Convert to JPEG";
|
||||
|
||||
"GroupInfo.MemberRequests" = "Member Requests";
|
||||
|
||||
"InviteLink.Create.RequestApproval" = "Request Admin Approval";
|
||||
"InviteLink.Create.RequestApprovalOffInfoGroup" = "New users will be able to join the group without being approved by the admins.";
|
||||
"InviteLink.Create.RequestApprovalOffInfoChannel" = "New users will be able to join the channel without being approved by the admins.";
|
||||
"InviteLink.Create.RequestApprovalOnInfoGroup" = "New users will be able to join the group only after having been approved by the admins.";
|
||||
"InviteLink.Create.RequestApprovalOnInfoChannel" = "New users will be able to join the channel only after having been approved by the admins.";
|
||||
|
||||
"MemberRequests.Title" = "Member Requests";
|
||||
"MemberRequests.DescriptionGroup" = "Some [additional links]() are set up to accept requests to join the group.";
|
||||
"MemberRequests.DescriptionChannel" = "Some [additional links]() are set up to accept requests to join the channel.";
|
||||
|
||||
"MemberRequests.PeopleRequested_1" = "%@ requested to join";
|
||||
"MemberRequests.PeopleRequested_2" = "%@ requested to join";
|
||||
"MemberRequests.PeopleRequested_3_10" = "%@ requested to join";
|
||||
"MemberRequests.PeopleRequested_many" = "%@ requested to join";
|
||||
"MemberRequests.PeopleRequested_any" = "%@ requested to join";
|
||||
|
||||
"MemberRequests.PeopleRequestedShort_1" = "%@ requested";
|
||||
"MemberRequests.PeopleRequestedShort_2" = "%@ requested";
|
||||
"MemberRequests.PeopleRequestedShort_3_10" = "%@ requested";
|
||||
"MemberRequests.PeopleRequestedShort_many" = "%@ requested";
|
||||
"MemberRequests.PeopleRequestedShort_any" = "%@ requested";
|
||||
|
||||
"MemberRequests.AddToGroup" = "Add to Group";
|
||||
"MemberRequests.AddToChannel" = "Add to Channel";
|
||||
"MemberRequests.Dismiss" = "Dismiss";
|
||||
|
||||
"MemberRequests.UserAddedToGroup" = "%@ has been added to the group.";
|
||||
"MemberRequests.UserAddedToChannel" = "%@ has been added to the channel.";
|
||||
|
||||
"MemberRequests.NoRequests" = "No Member Requests";
|
||||
"MemberRequests.NoRequestsDescriptionGroup" = "You have no pending requests to join the group.";
|
||||
"MemberRequests.NoRequestsDescriptionChannel" = "You have no pending requests to join the channel.";
|
||||
|
||||
"Conversation.RequestsToJoin_1" = "%@ Request to Join";
|
||||
"Conversation.RequestsToJoin_2" = "%@ Requests to Join";
|
||||
"Conversation.RequestsToJoin_3_10" = "%@ Requests to Join";
|
||||
"Conversation.RequestsToJoin_many" = "%@ Requests to Join";
|
||||
"Conversation.RequestsToJoin_any" = "%@ Requests to Join";
|
||||
|
||||
"MemberRequests.RequestToJoinGroup" = "Request to Join Group";
|
||||
"MemberRequests.RequestToJoinChannel" = "Request to Join Channel";
|
||||
|
||||
"MemberRequests.RequestToJoinDescriptionGroup" = "This group accepts new members only after they are approved by its admins.";
|
||||
"MemberRequests.RequestToJoinDescriptionChannel" = "This channel accepts new subscribers only after they are approved by its admins.";
|
||||
|
||||
"MemberRequests.RequestToJoinSent" = "Request to join Sent";
|
||||
"MemberRequests.RequestToJoinSentDescriptionGroup" = "You will be added to the group once it admins approve your request.";
|
||||
"MemberRequests.RequestToJoinSentDescriptionChannel" = "You will be added to the channel once it admins approve your request.";
|
||||
|
@ -1733,7 +1733,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var animateInputActivitiesFrame = false
|
||||
let inputActivities = inputActivities?.filter({
|
||||
switch $0.1 {
|
||||
case .speakingInGroupCall, .interactingWithEmoji, .seeingEmojiInteraction:
|
||||
case .speakingInGroupCall, .seeingEmojiInteraction:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
@ -60,7 +60,9 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
|
||||
text = strings.DialogList_Typing
|
||||
case .choosingSticker:
|
||||
text = strings.Activity_ChoosingSticker
|
||||
case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji:
|
||||
case let .interactingWithEmoji(emoticon, _, _):
|
||||
text = strings.Activity_TappingInteractiveEmoji(emoticon).string
|
||||
case .speakingInGroupCall, .seeingEmojiInteraction:
|
||||
text = ""
|
||||
}
|
||||
let string = NSAttributedString(string: text, font: textFont, textColor: color)
|
||||
@ -80,7 +82,9 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
|
||||
state = .typingText(string, lightColor)
|
||||
case .choosingSticker:
|
||||
state = .choosingSticker(string, lightColor)
|
||||
case .seeingEmojiInteraction, .interactingWithEmoji:
|
||||
case .interactingWithEmoji:
|
||||
state = .interactingWithEmoji(string, lightColor)
|
||||
case .seeingEmojiInteraction:
|
||||
state = .none
|
||||
}
|
||||
} else {
|
||||
|
@ -128,7 +128,7 @@ public class ChatTitleActivityContentNode: ASDisplayNode {
|
||||
if case .center = alignment {
|
||||
self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0 + offset)
|
||||
} else {
|
||||
self.textNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + offset)
|
||||
self.textNode.position = CGPoint(x: size.width / 2.0 + 3.0, y: size.height / 2.0 + offset)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ public enum ChatTitleActivityNodeState: Equatable {
|
||||
case recordingVideo(NSAttributedString, UIColor)
|
||||
case playingGame(NSAttributedString, UIColor)
|
||||
case choosingSticker(NSAttributedString, UIColor)
|
||||
case interactingWithEmoji(NSAttributedString, UIColor)
|
||||
|
||||
func contentNode() -> ChatTitleActivityContentNode? {
|
||||
switch self {
|
||||
@ -43,6 +44,8 @@ public enum ChatTitleActivityNodeState: Equatable {
|
||||
return ChatPlayingActivityContentNode(text: text, color: color)
|
||||
case let .choosingSticker(text, color):
|
||||
return ChatChoosingStickerActivityContentNode(text: text, color: color)
|
||||
case let .interactingWithEmoji(text, _):
|
||||
return ChatTitleActivityContentNode(text: text)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,10 +16,13 @@ private enum Constants {
|
||||
|
||||
private class LeftAlignedIconButton: UIButton {
|
||||
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
|
||||
let titleRect = super.titleRect(forContentRect: contentRect)
|
||||
var titleRect = super.titleRect(forContentRect: contentRect)
|
||||
let imageSize = currentImage?.size ?? .zero
|
||||
let availableWidth = contentRect.width - imageEdgeInsets.right - imageSize.width - titleRect.width
|
||||
return titleRect.offsetBy(dx: round(availableWidth / 2) - imageEdgeInsets.left, dy: 0)
|
||||
titleRect.origin.x = imageSize.width + 2.0
|
||||
//
|
||||
// let availableWidth = contentRect.width - imageEdgeInsets.right - imageSize.width - titleRect.width
|
||||
// return titleRect.offsetBy(dx: round(availableWidth / 2) - imageEdgeInsets.left, dy: 0)
|
||||
return titleRect
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,8 @@ swift_library(
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
"//submodules/LocalizedPeerData:LocalizedPeerData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -34,6 +34,7 @@ private final class InviteLinkEditControllerArguments {
|
||||
private enum InviteLinksEditSection: Int32 {
|
||||
case time
|
||||
case usage
|
||||
case requestApproval
|
||||
case revoke
|
||||
}
|
||||
|
||||
@ -65,10 +66,15 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool)
|
||||
case usageInfo(PresentationTheme, String)
|
||||
|
||||
case requestApproval(PresentationTheme, String, Bool)
|
||||
case requestApprovalInfo(PresentationTheme, String)
|
||||
|
||||
case revoke(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .requestApproval, .requestApprovalInfo:
|
||||
return InviteLinksEditSection.requestApproval.rawValue
|
||||
case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo:
|
||||
return InviteLinksEditSection.time.rawValue
|
||||
case .usageHeader, .usagePicker, .usageCustomPicker, .usageInfo:
|
||||
@ -80,26 +86,30 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .timeHeader:
|
||||
case .requestApproval:
|
||||
return 0
|
||||
case .timePicker:
|
||||
case .requestApprovalInfo:
|
||||
return 1
|
||||
case .timeExpiryDate:
|
||||
case .timeHeader:
|
||||
return 2
|
||||
case .timeCustomPicker:
|
||||
case .timePicker:
|
||||
return 3
|
||||
case .timeInfo:
|
||||
case .timeExpiryDate:
|
||||
return 4
|
||||
case .usageHeader:
|
||||
case .timeCustomPicker:
|
||||
return 5
|
||||
case .usagePicker:
|
||||
case .timeInfo:
|
||||
return 6
|
||||
case .usageCustomPicker:
|
||||
case .usageHeader:
|
||||
return 7
|
||||
case .usageInfo:
|
||||
case .usagePicker:
|
||||
return 8
|
||||
case .revoke:
|
||||
case .usageCustomPicker:
|
||||
return 9
|
||||
case .usageInfo:
|
||||
return 10
|
||||
case .revoke:
|
||||
return 11
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,6 +169,18 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .requestApproval(lhsTheme, lhsText, lhsValue):
|
||||
if case let .requestApproval(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .requestApprovalInfo(lhsTheme, lhsText):
|
||||
if case let .requestApprovalInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .revoke(lhsTheme, lhsText):
|
||||
if case let .revoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
@ -266,6 +288,16 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
})
|
||||
case let .usageInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .requestApproval(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.requestApproval = value
|
||||
return updatedState
|
||||
}
|
||||
})
|
||||
case let .requestApprovalInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .revoke(_, text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.revoke()
|
||||
@ -277,8 +309,16 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, presentationData: PresentationData) -> [InviteLinksEditEntry] {
|
||||
var entries: [InviteLinksEditEntry] = []
|
||||
|
||||
entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased()))
|
||||
entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval))
|
||||
var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel
|
||||
if state.requestApproval {
|
||||
requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel
|
||||
} else {
|
||||
requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel
|
||||
}
|
||||
entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText))
|
||||
|
||||
entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased()))
|
||||
entries.append(.timePicker(presentationData.theme, state.time))
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
@ -294,16 +334,17 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
|
||||
}
|
||||
entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo))
|
||||
|
||||
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
|
||||
entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage))
|
||||
|
||||
var customValue = false
|
||||
if case .custom = state.usage {
|
||||
customValue = true
|
||||
if !state.requestApproval {
|
||||
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
|
||||
entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage))
|
||||
|
||||
var customValue = false
|
||||
if case .custom = state.usage {
|
||||
customValue = true
|
||||
}
|
||||
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue))
|
||||
entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo))
|
||||
}
|
||||
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue))
|
||||
|
||||
entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo))
|
||||
|
||||
if let _ = invite {
|
||||
entries.append(.revoke(presentationData.theme, presentationData.strings.InviteLink_Create_Revoke))
|
||||
@ -315,6 +356,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
|
||||
private struct InviteLinkEditControllerState: Equatable {
|
||||
var usage: InviteLinkUsageLimit
|
||||
var time: InviteLinkTimeLimit
|
||||
var requestApproval = false
|
||||
var pickingTimeLimit = false
|
||||
var pickingUsageLimit = false
|
||||
var updating = false
|
||||
@ -343,9 +385,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
|
||||
timeLimit = .unlimited
|
||||
}
|
||||
|
||||
initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: invite.requestApproval, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
} else {
|
||||
initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, requestApproval: false, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
}
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
@ -443,8 +485,10 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
|
||||
}
|
||||
|
||||
let usageLimit = state.usage.value
|
||||
let requestNeeded = state.requestApproval
|
||||
|
||||
if invite == nil {
|
||||
let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|
||||
let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded)
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
@ -458,7 +502,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
|
||||
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
} else if let invite = invite {
|
||||
let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|
||||
let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded)
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
@ -476,7 +520,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
|
||||
|
||||
let previousState = previousState.swap(state)
|
||||
var animateChanges = false
|
||||
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit {
|
||||
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit || previousState.requestApproval != state.requestApproval {
|
||||
animateChanges = true
|
||||
}
|
||||
|
||||
|
@ -9,18 +9,22 @@ import PresentationDataUtils
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import AccountContext
|
||||
import Markdown
|
||||
import TextFormat
|
||||
|
||||
class InviteLinkHeaderItem: ListViewItem, ItemListItem {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let text: String
|
||||
let sectionId: ItemListSectionId
|
||||
let linkAction: ((ItemListTextItemLinkAction) -> Void)?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
|
||||
init(context: AccountContext, theme: PresentationTheme, text: String, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.sectionId = sectionId
|
||||
self.linkAction = linkAction
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
@ -82,6 +86,16 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
|
||||
self.addSubnode(self.animationNode)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { _ in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
@ -89,7 +103,10 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
|
||||
let leftInset: CGFloat = 32.0 + params.leftInset
|
||||
let topInset: CGFloat = 92.0
|
||||
|
||||
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}))
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
|
||||
@ -124,4 +141,27 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
let titleFrame = self.titleNode.frame
|
||||
if let item = self.item, titleFrame.contains(location) {
|
||||
if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
item.linkAction?(.tap(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData,
|
||||
let mainInvite: ExportedInvitation?
|
||||
var isPublic = false
|
||||
if let peer = peer, let address = peer.addressName, !address.isEmpty && admin == nil {
|
||||
mainInvite = ExportedInvitation(link: "t.me/\(address)", isPermanent: true, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil)
|
||||
mainInvite = ExportedInvitation(link: "t.me/\(address)", isPermanent: true, requestApproval: false, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, approvedDate: nil)
|
||||
isPublic = true
|
||||
} else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) {
|
||||
mainInvite = invite
|
||||
|
289
submodules/InviteLinksUI/Sources/InviteRequestsController.swift
Normal file
289
submodules/InviteLinksUI/Sources/InviteRequestsController.swift
Normal 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
|
||||
}
|
110
submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift
Normal file
110
submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift
Normal 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))
|
||||
}
|
||||
}
|
@ -344,7 +344,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
|
||||
if let invite = item.invite {
|
||||
let count = invite.count ?? 0
|
||||
if count > 0 {
|
||||
subtitleText = item.presentationData.strings.InviteLink_PeopleJoinedShort(count)
|
||||
if invite.requestApproval {
|
||||
subtitleText = item.presentationData.strings.MemberRequests_PeopleRequestedShort(count)
|
||||
} else {
|
||||
subtitleText = item.presentationData.strings.InviteLink_PeopleJoinedShort(count)
|
||||
}
|
||||
} else {
|
||||
if let usageLimit = invite.usageLimit, count == 0 && !availability.isZero {
|
||||
subtitleText = item.presentationData.strings.InviteLink_PeopleCanJoin(usageLimit)
|
||||
|
559
submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift
Normal file
559
submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ swift_library(
|
||||
"//submodules/SelectablePeerNode:SelectablePeerNode",
|
||||
"//submodules/PeerInfoUI:PeerInfoUI",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -20,6 +20,8 @@ public final class JoinLinkPreviewController: ViewController {
|
||||
|
||||
private let context: AccountContext
|
||||
private let link: String
|
||||
private var isRequest = false
|
||||
private var isGroup = false
|
||||
private let navigateToPeer: (EnginePeer.Id, ChatPeekTimeout?) -> Void
|
||||
private let parentNavigationController: NavigationController?
|
||||
private var resolvedState: ExternalJoiningChatState?
|
||||
@ -76,9 +78,15 @@ public final class JoinLinkPreviewController: ViewController {
|
||||
if let strongSelf = self {
|
||||
strongSelf.resolvedState = result
|
||||
switch result {
|
||||
case let .invite(title, photoRepresentation, participantsCount, participants):
|
||||
let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false)
|
||||
strongSelf.controllerNode.setPeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants?.map({ EnginePeer($0) }) ?? [], data: data)
|
||||
case let .invite(flags, title, about, photoRepresentation, participantsCount, participants):
|
||||
if flags.requestNeeded {
|
||||
strongSelf.isRequest = true
|
||||
strongSelf.isGroup = !flags.isBroadcast
|
||||
strongSelf.controllerNode.setRequestPeer(image: photoRepresentation, title: title, about: about, memberCount: participantsCount, isGroup: !flags.isBroadcast)
|
||||
} else {
|
||||
let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false)
|
||||
strongSelf.controllerNode.setInvitePeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants?.map({ EnginePeer($0) }) ?? [], data: data)
|
||||
}
|
||||
case let .alreadyJoined(peerId):
|
||||
strongSelf.navigateToPeer(peerId, nil)
|
||||
strongSelf.dismiss()
|
||||
@ -121,10 +129,14 @@ public final class JoinLinkPreviewController: ViewController {
|
||||
private func join() {
|
||||
self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
if let peerId = peerId {
|
||||
strongSelf.navigateToPeer(peerId, nil)
|
||||
strongSelf.dismiss()
|
||||
if strongSelf.isRequest {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: strongSelf.isGroup ? strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
} else {
|
||||
if let peerId = peerId {
|
||||
strongSelf.navigateToPeer(peerId, nil)
|
||||
}
|
||||
}
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
|
@ -8,6 +8,27 @@ import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ShareController
|
||||
|
||||
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
struct JoinLinkPreviewData {
|
||||
let isGroup: Bool
|
||||
let isJoined: Bool
|
||||
@ -24,18 +45,17 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
private let wrappingScrollNode: ASScrollNode
|
||||
private let cancelButtonNode: ASButtonNode
|
||||
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
private let contentBackgroundNode: ASImageNode
|
||||
private let effectNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let contentBackgroundNode: ASDisplayNode
|
||||
|
||||
private var contentNode: (ASDisplayNode & ShareContentContainerNode)?
|
||||
private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)?
|
||||
private var animateContentNodeOffsetFromBackgroundOffset: CGFloat?
|
||||
|
||||
private let actionsBackgroundNode: ASImageNode
|
||||
private let actionButtonNode: ShareActionButtonNode
|
||||
private let actionSeparatorNode: ASDisplayNode
|
||||
private let cancelButton: HighlightableButtonNode
|
||||
|
||||
var dismiss: (() -> Void)?
|
||||
var cancel: (() -> Void)?
|
||||
@ -51,28 +71,12 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
init(context: AccountContext, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.requestLayout = requestLayout
|
||||
|
||||
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
|
||||
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
|
||||
|
||||
let theme = self.presentationData.theme
|
||||
let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0)))
|
||||
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
|
||||
|
||||
let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0)))
|
||||
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
|
||||
|
||||
self.wrappingScrollNode = ASScrollNode()
|
||||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
@ -81,35 +85,22 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
self.cancelButtonNode = ASButtonNode()
|
||||
self.cancelButtonNode.displaysAsynchronously = false
|
||||
self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal)
|
||||
self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted)
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.isOpaque = false
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
|
||||
self.contentBackgroundNode = ASImageNode()
|
||||
self.contentBackgroundNode.displaysAsynchronously = false
|
||||
self.contentBackgroundNode.displayWithoutProcessing = true
|
||||
self.contentBackgroundNode.image = roundedBackground
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
self.backgroundNode.cornerRadius = 16.0
|
||||
|
||||
self.actionsBackgroundNode = ASImageNode()
|
||||
self.actionsBackgroundNode.isLayerBacked = true
|
||||
self.actionsBackgroundNode.displayWithoutProcessing = true
|
||||
self.actionsBackgroundNode.displaysAsynchronously = false
|
||||
self.actionsBackgroundNode.image = halfRoundedBackground
|
||||
self.effectNode = ASDisplayNode(viewBlock: {
|
||||
return UIVisualEffectView(effect: UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark))
|
||||
})
|
||||
|
||||
self.actionButtonNode = ShareActionButtonNode(badgeBackgroundColor: self.presentationData.theme.actionSheet.controlAccentColor, badgeTextColor: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
|
||||
self.actionButtonNode.displaysAsynchronously = false
|
||||
self.actionButtonNode.titleNode.displaysAsynchronously = false
|
||||
self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted)
|
||||
|
||||
self.actionSeparatorNode = ASDisplayNode()
|
||||
self.actionSeparatorNode.isLayerBacked = true
|
||||
self.actionSeparatorNode.displaysAsynchronously = false
|
||||
self.actionSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
|
||||
self.contentBackgroundNode = ASDisplayNode()
|
||||
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
|
||||
|
||||
self.cancelButton = HighlightableButtonNode()
|
||||
self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
|
||||
|
||||
super.init()
|
||||
|
||||
@ -121,27 +112,20 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.wrappingScrollNode.view.delegate = self
|
||||
self.addSubnode(self.wrappingScrollNode)
|
||||
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
|
||||
|
||||
self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.cancelButtonNode)
|
||||
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.actionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.contentBackgroundNode)
|
||||
self.backgroundNode.addSubnode(self.effectNode)
|
||||
self.backgroundNode.addSubnode(self.contentBackgroundNode)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.backgroundNode)
|
||||
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
|
||||
self.contentContainerNode.addSubnode(self.actionSeparatorNode)
|
||||
self.contentContainerNode.addSubnode(self.actionsBackgroundNode)
|
||||
self.contentContainerNode.addSubnode(self.actionButtonNode)
|
||||
|
||||
self.transitionToContentNode(ShareLoadingContainerNode(theme: theme, forceNativeAppearance: false))
|
||||
|
||||
self.actionButtonNode.alpha = 0.0
|
||||
self.actionSeparatorNode.alpha = 0.0
|
||||
self.actionsBackgroundNode.alpha = 0.0
|
||||
self.wrappingScrollNode.addSubnode(self.cancelButton)
|
||||
|
||||
self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false))
|
||||
|
||||
self.ready.set(.single(true))
|
||||
self.didSetReady = true
|
||||
}
|
||||
@ -230,45 +214,31 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
||||
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
if insets.bottom > 0 {
|
||||
if insets.bottom > 0.0 {
|
||||
bottomInset -= 12.0
|
||||
}
|
||||
let buttonHeight: CGFloat = 57.0
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
let titleAreaHeight: CGFloat = 64.0
|
||||
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset, insets.bottom)
|
||||
|
||||
let width = min(layout.size.width, layout.size.height) - 20.0
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
|
||||
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
|
||||
let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0)
|
||||
|
||||
let bottomGridInset = buttonHeight
|
||||
|
||||
self.containerLayout = (layout, navigationBarHeight, bottomGridInset)
|
||||
let contentFrame = contentContainerFrame
|
||||
|
||||
self.containerLayout = (layout, navigationBarHeight, 0.0)
|
||||
self.scheduledLayoutTransitionRequest = nil
|
||||
|
||||
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight)))
|
||||
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
|
||||
transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset)))
|
||||
|
||||
transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
|
||||
|
||||
transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
|
||||
|
||||
let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight))
|
||||
let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height))
|
||||
|
||||
if let contentNode = self.contentNode {
|
||||
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize))
|
||||
contentNode.updateLayout(size: gridSize, bottomInset: bottomGridInset, transition: transition)
|
||||
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: 0.0), size: gridSize))
|
||||
contentNode.updateLayout(size: gridSize, bottomInset: 0.0, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,14 +252,11 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
if insets.bottom > 0 {
|
||||
bottomInset -= 12.0
|
||||
}
|
||||
let buttonHeight: CGFloat = 57.0
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
|
||||
let width = min(layout.size.width, layout.size.height) - 20.0
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset, insets.bottom)
|
||||
let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - contentOffset), size: contentFrame.size)
|
||||
@ -299,11 +266,15 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
if backgroundFrame.maxY > contentFrame.maxY {
|
||||
backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY
|
||||
}
|
||||
if backgroundFrame.size.height < buttonHeight + 32.0 {
|
||||
backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height
|
||||
backgroundFrame.size.height = buttonHeight + 32.0
|
||||
}
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame)
|
||||
backgroundFrame.size.height += 2000.0
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
|
||||
let cancelSize = CGSize(width: 44.0, height: 44.0)
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - cancelSize.width - 3.0, y: backgroundFrame.minY + 6.0), size: cancelSize)
|
||||
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
|
||||
|
||||
if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset {
|
||||
self.animateContentNodeOffsetFromBackgroundOffset = nil
|
||||
@ -328,10 +299,6 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
self.cancel?()
|
||||
}
|
||||
|
||||
@objc func installActionButtonPressed() {
|
||||
self.join?()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if self.contentNode != nil {
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
@ -382,11 +349,8 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = self.actionButtonNode.hitTest(self.actionButtonNode.convert(point, from: self), with: event) {
|
||||
return result
|
||||
}
|
||||
if self.bounds.contains(point) {
|
||||
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) {
|
||||
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButton.bounds.contains(self.convert(point, to: self.cancelButton)) {
|
||||
return self.dimNode.view
|
||||
}
|
||||
}
|
||||
@ -430,12 +394,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
func transitionToProgress(signal: Signal<Void, NoError>) {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0)
|
||||
|
||||
func transitionToProgress(signal: Signal<Void, NoError>) {
|
||||
self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false), fastOut: true)
|
||||
let timestamp = CACurrentMediaTime()
|
||||
self.disposable.set(signal.start(completed: { [weak self] in
|
||||
@ -449,19 +408,19 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}))
|
||||
}
|
||||
|
||||
func setPeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.actionButtonNode, alpha: 1.0)
|
||||
transition.updateAlpha(node: self.actionSeparatorNode, alpha: 1.0)
|
||||
transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 1.0)
|
||||
|
||||
self.actionButtonNode.isEnabled = true
|
||||
if data.isJoined {
|
||||
self.actionButtonNode.setTitle(self.presentationData.strings.Conversation_LinkDialogOpen, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
|
||||
} else {
|
||||
self.actionButtonNode.setTitle(data.isGroup ? self.presentationData.strings.Invitation_JoinGroup : self.presentationData.strings.Channel_JoinChannel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
|
||||
func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) {
|
||||
let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, memberCount: memberCount, members: members))
|
||||
contentNode.join = { [weak self] in
|
||||
self?.join?()
|
||||
}
|
||||
|
||||
self.transitionToContentNode(JoinLinkPreviewPeerContentNode(context: self.context, image: image, title: title, memberCount: memberCount, members: members, isGroup: data.isGroup, theme: self.presentationData.theme, strings: self.presentationData.strings))
|
||||
self.transitionToContentNode(contentNode)
|
||||
}
|
||||
|
||||
func setRequestPeer(image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, isGroup: Bool) {
|
||||
let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .request(isGroup: isGroup, image: image, title: title, about: about, memberCount: memberCount))
|
||||
contentNode.join = { [weak self] in
|
||||
self?.join?()
|
||||
}
|
||||
self.transitionToContentNode(contentNode)
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ import AvatarNode
|
||||
import AccountContext
|
||||
import SelectablePeerNode
|
||||
import ShareController
|
||||
import SolidRoundedButtonNode
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 26.0)
|
||||
private let avatarFont = avatarPlaceholderFont(size: 42.0)
|
||||
|
||||
private final class MoreNode: ASDisplayNode {
|
||||
private let avatarNode = AvatarNode(font: Font.regular(24.0))
|
||||
@ -27,61 +28,116 @@ private final class MoreNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode {
|
||||
enum Content {
|
||||
case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer])
|
||||
case request(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32)
|
||||
|
||||
var isGroup: Bool {
|
||||
switch self {
|
||||
case let .invite(isGroup, _, _, _, _), let .request(isGroup, _, _, _, _):
|
||||
return isGroup
|
||||
}
|
||||
}
|
||||
|
||||
var image: TelegramMediaImageRepresentation? {
|
||||
switch self {
|
||||
case let .invite(_, image, _, _, _), let .request(_, image, _, _, _):
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case let .invite(_, _, title, _, _), let .request(_, _, title, _, _):
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
var memberCount: Int32 {
|
||||
switch self {
|
||||
case let .invite(_, _, _, memberCount, _), let .request(_, _, _, _, memberCount):
|
||||
return memberCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
private let avatarNode: AvatarNode
|
||||
private let titleNode: ASTextNode
|
||||
private let countNode: ASTextNode
|
||||
private let aboutNode: ASTextNode
|
||||
private let descriptionNode: ASTextNode
|
||||
private let peersScrollNode: ASScrollNode
|
||||
|
||||
private let peerNodes: [SelectablePeerNode]
|
||||
private let moreNode: MoreNode?
|
||||
|
||||
init(context: AccountContext, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], isGroup: Bool, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
private let actionButtonNode: SolidRoundedButtonNode
|
||||
|
||||
var join: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: JoinLinkPreviewPeerContentNode.Content) {
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.titleNode = ASTextNode()
|
||||
self.countNode = ASTextNode()
|
||||
self.aboutNode = ASTextNode()
|
||||
self.aboutNode.maximumNumberOfLines = 6
|
||||
self.descriptionNode = ASTextNode()
|
||||
self.descriptionNode.maximumNumberOfLines = 0
|
||||
self.descriptionNode.textAlignment = .center
|
||||
self.peersScrollNode = ASScrollNode()
|
||||
self.peersScrollNode.view.showsHorizontalScrollIndicator = false
|
||||
|
||||
self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0, gloss: false)
|
||||
|
||||
let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor)
|
||||
|
||||
self.peerNodes = members.map { peer in
|
||||
let node = SelectablePeerNode()
|
||||
node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), synchronousLoad: false)
|
||||
node.theme = itemTheme
|
||||
return node
|
||||
}
|
||||
|
||||
if members.count < Int(memberCount) {
|
||||
self.moreNode = MoreNode(count: Int(memberCount) - members.count)
|
||||
if case let .invite(isGroup, _, _, memberCount, members) = content {
|
||||
self.peerNodes = members.map { peer in
|
||||
let node = SelectablePeerNode()
|
||||
node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), synchronousLoad: false)
|
||||
node.theme = itemTheme
|
||||
return node
|
||||
}
|
||||
|
||||
if members.count < Int(memberCount) {
|
||||
self.moreNode = MoreNode(count: Int(memberCount) - members.count)
|
||||
} else {
|
||||
self.moreNode = nil
|
||||
}
|
||||
|
||||
self.actionButtonNode.title = isGroup ? strings.Invitation_JoinGroup : strings.Channel_JoinChannel
|
||||
} else {
|
||||
self.peerNodes = []
|
||||
self.moreNode = nil
|
||||
|
||||
self.actionButtonNode.title = content.isGroup ? strings.MemberRequests_RequestToJoinGroup : strings.MemberRequests_RequestToJoinChannel
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
let peer = TelegramGroup(id: EnginePeer.Id(0), title: title, photo: image.flatMap { [$0] } ?? [], participantCount: Int(memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
|
||||
let peer = TelegramGroup(id: EnginePeer.Id(0), title: content.title, photo: content.image.flatMap { [$0] } ?? [], participantCount: Int(content.memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: content.title, font: Font.semibold(24.0), textColor: theme.actionSheet.primaryTextColor)
|
||||
|
||||
self.addSubnode(self.countNode)
|
||||
let membersString: String
|
||||
if isGroup {
|
||||
if !members.isEmpty {
|
||||
if content.isGroup {
|
||||
if case let .invite(_, _, _, memberCount, members) = content, !members.isEmpty {
|
||||
membersString = strings.Invitation_Members(memberCount)
|
||||
} else {
|
||||
membersString = strings.Conversation_StatusMembers(memberCount)
|
||||
membersString = strings.Conversation_StatusMembers(content.memberCount)
|
||||
}
|
||||
} else {
|
||||
membersString = strings.Conversation_StatusSubscribers(memberCount)
|
||||
membersString = strings.Conversation_StatusSubscribers(content.memberCount)
|
||||
}
|
||||
|
||||
self.countNode.attributedText = NSAttributedString(string: membersString, font: Font.regular(16.0), textColor: theme.actionSheet.secondaryTextColor)
|
||||
self.countNode.attributedText = NSAttributedString(string: membersString, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor)
|
||||
|
||||
if !self.peerNodes.isEmpty {
|
||||
for peerNode in peerNodes {
|
||||
@ -90,6 +146,21 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
||||
self.addSubnode(self.peersScrollNode)
|
||||
}
|
||||
self.moreNode.flatMap(self.peersScrollNode.addSubnode)
|
||||
|
||||
if case let .request(isGroup, _, _, about, _) = content {
|
||||
if let about = about, !about.isEmpty {
|
||||
self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor)
|
||||
self.addSubnode(self.aboutNode)
|
||||
}
|
||||
|
||||
self.descriptionNode.attributedText = NSAttributedString(string: isGroup ? strings.MemberRequests_RequestToJoinDescriptionGroup : strings.MemberRequests_RequestToJoinDescriptionChannel, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor, paragraphAlignment: .center)
|
||||
self.addSubnode(self.descriptionNode)
|
||||
}
|
||||
|
||||
self.actionButtonNode.pressed = { [weak self] in
|
||||
self?.join?()
|
||||
}
|
||||
self.addSubnode(self.actionButtonNode)
|
||||
}
|
||||
|
||||
func activate() {
|
||||
@ -106,19 +177,41 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let nodeHeight: CGFloat = self.peerNodes.isEmpty ? 224.0 : 324.0
|
||||
var nodeHeight: CGFloat = (self.peerNodes.isEmpty ? 264.0 : 364.0)
|
||||
|
||||
let paddedSize = CGSize(width: size.width - 60.0, height: size.height)
|
||||
|
||||
var aboutSize: CGSize?
|
||||
var descriptionSize: CGSize?
|
||||
if self.aboutNode.supernode != nil {
|
||||
let measuredSize = self.aboutNode.measure(paddedSize)
|
||||
nodeHeight += measuredSize.height + 20.0
|
||||
aboutSize = measuredSize
|
||||
}
|
||||
if self.descriptionNode.supernode != nil {
|
||||
let measuredSize = self.descriptionNode.measure(paddedSize)
|
||||
nodeHeight += measuredSize.height + 20.0 + 10.0
|
||||
descriptionSize = measuredSize
|
||||
}
|
||||
|
||||
let verticalOrigin = size.height - nodeHeight
|
||||
|
||||
let avatarSize: CGFloat = 75.0
|
||||
let avatarSize: CGFloat = 100.0
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 22.0), size: CGSize(width: avatarSize, height: avatarSize)))
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 32.0), size: CGSize(width: avatarSize, height: avatarSize)))
|
||||
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0), size: titleSize))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0), size: titleSize))
|
||||
|
||||
let countSize = self.countNode.measure(size)
|
||||
transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 22.0 + avatarSize + 15.0 + titleSize.height + 1.0), size: countSize))
|
||||
transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0), size: countSize))
|
||||
|
||||
var verticalOffset = verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 18.0
|
||||
|
||||
if let aboutSize = aboutSize {
|
||||
transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize))
|
||||
verticalOffset += aboutSize.height + 20.0
|
||||
}
|
||||
|
||||
let peerSize = CGSize(width: 85.0, height: 95.0)
|
||||
let peerInset: CGFloat = 10.0
|
||||
@ -136,9 +229,22 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
||||
}
|
||||
|
||||
self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + (self.moreNode != nil ? peerSize.width : 0.0) + peerInset * 2.0, height: peerSize.height)
|
||||
transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 168.0), size: CGSize(width: size.width, height: peerSize.height)))
|
||||
transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 210.0), size: CGSize(width: size.width, height: peerSize.height)))
|
||||
|
||||
self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition)
|
||||
if !self.peerNodes.isEmpty {
|
||||
verticalOffset += 100.0
|
||||
}
|
||||
|
||||
let buttonInset: CGFloat = 16.0
|
||||
let actionButtonHeight = self.actionButtonNode.updateLayout(width: size.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.actionButtonNode, frame: CGRect(x: buttonInset, y: verticalOffset, width: size.width, height: actionButtonHeight))
|
||||
verticalOffset += actionButtonHeight + 20.0
|
||||
|
||||
if let descriptionSize = descriptionSize {
|
||||
transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: verticalOffset), size: descriptionSize))
|
||||
}
|
||||
|
||||
self.contentOffsetUpdated?(-size.height + nodeHeight, transition)
|
||||
}
|
||||
|
||||
func updateSelectedPeers() {
|
||||
|
@ -266,7 +266,9 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati
|
||||
}
|
||||
};
|
||||
carouselItem.allowCaptions = true
|
||||
carouselItem.editingContext.setForcedCaption(initialCaption, entities: [])
|
||||
if !initialCaption.isEmpty {
|
||||
carouselItem.editingContext.setForcedCaption(initialCaption, entities: [])
|
||||
}
|
||||
itemViews.append(carouselItem)
|
||||
|
||||
let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
|
||||
|
@ -60,7 +60,9 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co
|
||||
controller.shouldShowFileTipIfNeeded = showFileTooltip
|
||||
controller.requestSearchController = presentWebSearch
|
||||
|
||||
controller.editingContext.setForcedCaption(initialCaption, entities: [])
|
||||
if !initialCaption.isEmpty {
|
||||
controller.editingContext.setForcedCaption(initialCaption, entities: [])
|
||||
}
|
||||
}
|
||||
|
||||
public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
|
||||
|
@ -14,6 +14,7 @@ import LocationResources
|
||||
import AppBundle
|
||||
import AvatarNode
|
||||
import LiveLocationTimerNode
|
||||
import SolidRoundedButtonNode
|
||||
|
||||
final class LocationLiveListItem: ListViewItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
@ -22,18 +23,33 @@ final class LocationLiveListItem: ListViewItem {
|
||||
let context: AccountContext
|
||||
let message: Message
|
||||
let distance: Double?
|
||||
|
||||
let drivingTime: Double?
|
||||
let transitTime: Double?
|
||||
let walkingTime: Double?
|
||||
|
||||
let action: () -> Void
|
||||
let longTapAction: () -> Void
|
||||
|
||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: Message, distance: Double?, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }) {
|
||||
let drivingAction: () -> Void
|
||||
let transitAction: () -> Void
|
||||
let walkingAction: () -> Void
|
||||
|
||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: Message, distance: Double?, drivingTime: Double?, transitTime: Double?, walkingTime: Double?, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
self.context = context
|
||||
self.message = message
|
||||
self.distance = distance
|
||||
self.drivingTime = drivingTime
|
||||
self.transitTime = transitTime
|
||||
self.walkingTime = walkingTime
|
||||
self.action = action
|
||||
self.longTapAction = longTapAction
|
||||
self.drivingAction = drivingAction
|
||||
self.transitAction = transitAction
|
||||
self.walkingAction = walkingAction
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
@ -84,6 +100,10 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
||||
private let avatarNode: AvatarNode
|
||||
private var timerNode: ChatMessageLiveLocationTimerNode?
|
||||
|
||||
private var drivingButtonNode: SolidRoundedButtonNode?
|
||||
private var transitButtonNode: SolidRoundedButtonNode?
|
||||
private var walkingButtonNode: SolidRoundedButtonNode?
|
||||
|
||||
private var item: LocationLiveListItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
@ -99,7 +119,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
@ -185,7 +205,10 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 54.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height)
|
||||
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height)
|
||||
if item.drivingTime != nil || item.transitTime != nil || item.walkingTime != nil {
|
||||
contentSize.height += 46.0
|
||||
}
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
return (nodeLayout, { [weak self] in
|
||||
@ -217,6 +240,43 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
||||
strongSelf.addSubnode(subtitleNode)
|
||||
}
|
||||
|
||||
let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
|
||||
if strongSelf.drivingButtonNode == nil {
|
||||
strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsDriving"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.drivingButtonNode?.alpha = 0.0
|
||||
strongSelf.drivingButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.drivingButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.drivingAction()
|
||||
}
|
||||
}
|
||||
strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
|
||||
strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsTransit"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.transitButtonNode?.alpha = 0.0
|
||||
strongSelf.transitButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.transitButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.transitAction()
|
||||
}
|
||||
}
|
||||
strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
|
||||
strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsWalking"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.walkingButtonNode?.alpha = 0.0
|
||||
strongSelf.walkingButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.walkingButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.walkingAction()
|
||||
}
|
||||
}
|
||||
strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
} else if let _ = updatedTheme {
|
||||
strongSelf.drivingButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.transitButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.walkingButtonNode?.updateTheme(buttonTheme)
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
|
||||
titleNode.frame = titleFrame
|
||||
|
||||
@ -231,7 +291,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(peer), overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false)
|
||||
}
|
||||
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
|
||||
@ -255,11 +315,48 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
||||
}
|
||||
let timerSize = CGSize(width: 28.0, height: 28.0)
|
||||
timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(liveBroadcastingTimeout), strings: item.presentationData.strings)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0)), size: timerSize)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize)
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
timerNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let drivingTime = item.drivingTime {
|
||||
strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 })
|
||||
|
||||
if currentItem?.drivingTime == nil {
|
||||
strongSelf.drivingButtonNode?.alpha = 1.0
|
||||
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
if let transitTime = item.transitTime {
|
||||
strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 })
|
||||
|
||||
if currentItem?.transitTime == nil {
|
||||
strongSelf.transitButtonNode?.alpha = 1.0
|
||||
strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
if let walkingTime = item.walkingTime {
|
||||
strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 })
|
||||
|
||||
if currentItem?.walkingTime == nil {
|
||||
strongSelf.walkingButtonNode?.alpha = 1.0
|
||||
strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
let directionsWidth: CGFloat = 93.0
|
||||
let directionsSpacing: CGFloat = 8.0
|
||||
let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
|
||||
strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight))
|
||||
strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset + directionsWidth + directionsSpacing, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight))
|
||||
strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset + directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -70,11 +70,11 @@ public func nearbyVenues(context: AccountContext, latitude: Double, longitude: D
|
||||
}
|
||||
}
|
||||
|
||||
func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> String? {
|
||||
if eta > 0.0 && eta < 60.0 * 60.0 * 10.0 {
|
||||
let eta = max(eta, 60.0)
|
||||
let minutes = Int32(eta / 60.0) % 60
|
||||
let hours = Int32(eta / 3600.0)
|
||||
func stringForEstimatedDuration(strings: PresentationStrings, time: Double, format: (String) -> String) -> String? {
|
||||
if time > 0.0 {
|
||||
let time = max(time, 60.0)
|
||||
let minutes = Int32(time / 60.0) % 60
|
||||
let hours = Int32(time / 3600.0)
|
||||
|
||||
let string: String
|
||||
if hours > 1 {
|
||||
@ -86,7 +86,7 @@ func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> St
|
||||
} else {
|
||||
string = strings.Map_ETAMinutes(minutes)
|
||||
}
|
||||
return strings.Map_DirectionsDriveEta(string).string
|
||||
return format(string)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -117,7 +117,7 @@ func throttledUserLocation(_ userLocation: Signal<CLLocation?, NoError>) -> Sign
|
||||
}
|
||||
}
|
||||
|
||||
func driveEta(coordinate: CLLocationCoordinate2D) -> Signal<Double?, NoError> {
|
||||
func getExpectedTravelTime(coordinate: CLLocationCoordinate2D, transportType: MKDirectionsTransportType) -> Signal<Double?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let destinationPlacemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil)
|
||||
let destination = MKMapItem(placemark: destinationPlacemark)
|
||||
@ -125,7 +125,7 @@ func driveEta(coordinate: CLLocationCoordinate2D) -> Signal<Double?, NoError> {
|
||||
let request = MKDirections.Request()
|
||||
request.source = MKMapItem.forCurrentLocation()
|
||||
request.destination = destination
|
||||
request.transportType = .automobile
|
||||
request.transportType = transportType
|
||||
request.requestsAlternateRoutes = false
|
||||
|
||||
let directions = MKDirections(request: request)
|
||||
|
@ -15,6 +15,7 @@ import OpenInExternalAppUI
|
||||
import ShareController
|
||||
import DeviceAccess
|
||||
import UndoUI
|
||||
import MapKit
|
||||
|
||||
public class LocationViewParams {
|
||||
let sendLiveLocation: (TelegramMediaMap) -> Void
|
||||
@ -43,7 +44,7 @@ class LocationViewInteraction {
|
||||
let updateMapMode: (LocationMapMode) -> Void
|
||||
let toggleTrackingMode: () -> Void
|
||||
let goToCoordinate: (CLLocationCoordinate2D) -> Void
|
||||
let requestDirections: () -> Void
|
||||
let requestDirections: (TelegramMediaMap, String?, OpenInLocationDirections) -> Void
|
||||
let share: () -> Void
|
||||
let setupProximityNotification: (Bool, MessageId?) -> Void
|
||||
let updateSendActionHighlight: (Bool) -> Void
|
||||
@ -52,7 +53,7 @@ class LocationViewInteraction {
|
||||
let updateRightBarButton: (LocationViewRightBarButton) -> Void
|
||||
let present: (ViewController) -> Void
|
||||
|
||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, MessageId?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, MessageId?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
self.toggleMapModeSelection = toggleMapModeSelection
|
||||
self.updateMapMode = updateMapMode
|
||||
self.toggleTrackingMode = toggleTrackingMode
|
||||
@ -161,12 +162,31 @@ public final class LocationViewController: ViewController {
|
||||
state.selectedLocation = .coordinate(coordinate, false)
|
||||
return state
|
||||
}
|
||||
}, requestDirections: { [weak self] in
|
||||
}, requestDirections: { [weak self] location, peerName, directions in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let location = getLocation(from: strongSelf.subject) {
|
||||
strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
let item: OpenInItem = .location(location: location, directions: directions)
|
||||
let openInOptions = availableOpenInOptions(context: context, item: item)
|
||||
if openInOptions.count == 1, let action = openInOptions.first?.action() {
|
||||
if case let .openLocation(latitude, longitude, directions) = action {
|
||||
let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:])
|
||||
let mapItem = MKMapItem(placemark: placemark)
|
||||
if let title = location.venue?.title {
|
||||
mapItem.name = title
|
||||
} else if let peerName = peerName {
|
||||
mapItem.name = peerName
|
||||
}
|
||||
|
||||
if let directions = directions {
|
||||
let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ]
|
||||
MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options)
|
||||
} else {
|
||||
mapItem.openInMaps(launchOptions: nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: directions), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
}
|
||||
}, share: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -176,7 +196,7 @@ public final class LocationViewController: ViewController {
|
||||
let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: {
|
||||
strongSelf.present(ShareController(context: context, subject: .mapMedia(location), externalShare: true), in: .window(.root), with: nil)
|
||||
})
|
||||
strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
}
|
||||
}, setupProximityNotification: { [weak self] reset, messageId in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -37,6 +37,7 @@ private struct LocationViewTransaction {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let gotTravelTimes: Bool
|
||||
}
|
||||
|
||||
private enum LocationViewEntryId: Hashable {
|
||||
@ -48,7 +49,7 @@ private enum LocationViewEntryId: Hashable {
|
||||
private enum LocationViewEntry: Comparable, Identifiable {
|
||||
case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?)
|
||||
case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?)
|
||||
case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, Message, Double?, Int)
|
||||
case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, Message, Double?, Double?, Double?, Double?, Int)
|
||||
|
||||
var stableId: LocationViewEntryId {
|
||||
switch self {
|
||||
@ -56,7 +57,7 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
return .info
|
||||
case .toggleLiveLocation:
|
||||
return .toggleLiveLocation
|
||||
case let .liveLocation(_, _, _, message, _, _):
|
||||
case let .liveLocation(_, _, _, message, _, _, _, _, _):
|
||||
return .liveLocation(message.stableId)
|
||||
}
|
||||
}
|
||||
@ -75,8 +76,8 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsIndex):
|
||||
if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsIndex == rhsIndex {
|
||||
case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex):
|
||||
if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -100,11 +101,11 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
case .liveLocation:
|
||||
return true
|
||||
}
|
||||
case let .liveLocation(_, _, _, _, _, lhsIndex):
|
||||
case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex):
|
||||
switch rhs {
|
||||
case .info, .toggleLiveLocation:
|
||||
return false
|
||||
case let .liveLocation(_, _, _, _, _, rhsIndex):
|
||||
case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
@ -125,11 +126,14 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
distanceString = nil
|
||||
}
|
||||
let eta = time.flatMap { stringForEstimatedDuration(strings: presentationData.strings, eta: $0) }
|
||||
var eta: String?
|
||||
if let time = time, time < 60.0 * 60.0 * 10.0 {
|
||||
eta = stringForEstimatedDuration(strings: presentationData.strings, time: time, format: { presentationData.strings.Map_DirectionsDriveEta($0).string })
|
||||
}
|
||||
return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, eta: eta, action: {
|
||||
interaction?.goToCoordinate(location.coordinate)
|
||||
}, getDirections: {
|
||||
interaction?.requestDirections()
|
||||
interaction?.requestDirections(location, nil, .driving)
|
||||
})
|
||||
case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout):
|
||||
let beginTimeAndTimeout: (Double, Double)?
|
||||
@ -147,24 +151,40 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
}, highlighted: { highlight in
|
||||
interaction?.updateSendActionHighlight(highlight)
|
||||
})
|
||||
case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, _):
|
||||
return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, action: {
|
||||
case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _):
|
||||
var title: String?
|
||||
if let author = message.author {
|
||||
title = EnginePeer(author).displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder)
|
||||
}
|
||||
return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: {
|
||||
if let location = getLocation(from: message) {
|
||||
interaction?.goToCoordinate(location.coordinate)
|
||||
}
|
||||
}, longTapAction: {})
|
||||
}, longTapAction: {}, drivingAction: {
|
||||
if let location = getLocation(from: message) {
|
||||
interaction?.requestDirections(location, title, .driving)
|
||||
}
|
||||
}, transitAction: {
|
||||
if let location = getLocation(from: message) {
|
||||
interaction?.requestDirections(location, title, .transit)
|
||||
}
|
||||
}, walkingAction: {
|
||||
if let location = getLocation(from: message) {
|
||||
interaction?.requestDirections(location, title, .walking)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> LocationViewTransaction {
|
||||
private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?, gotTravelTimes: Bool) -> LocationViewTransaction {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates)
|
||||
return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes)
|
||||
}
|
||||
|
||||
enum LocationViewLocation: Equatable {
|
||||
@ -217,6 +237,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
|
||||
var reportedAnnotationsReady = false
|
||||
var onAnnotationsReady: (() -> Void)?
|
||||
|
||||
private let travelDisposables = DisposableSet()
|
||||
private var travelTimes: [EngineMessage.Id: (Double, Double?, Double?, Double?)] = [:] {
|
||||
didSet {
|
||||
self.travelTimesPromise.set(.single(self.travelTimes))
|
||||
}
|
||||
}
|
||||
private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, Double?, Double?, Double?)]>([:])
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, subject: Message, interaction: LocationViewInteraction, locationManager: LocationManager) {
|
||||
self.context = context
|
||||
@ -263,7 +291,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
|
||||
if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil {
|
||||
eta = .single(nil)
|
||||
|> then(driveEta(coordinate: location.coordinate))
|
||||
|> then(getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile))
|
||||
|
||||
if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty {
|
||||
address = .single(venueAddress)
|
||||
@ -304,13 +332,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
let previousUserAnnotation = Atomic<LocationPinAnnotation?>(value: nil)
|
||||
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
|
||||
let previousEntries = Atomic<[LocationViewEntry]?>(value: nil)
|
||||
let previousHadTravelTimes = Atomic<Bool>(value: false)
|
||||
|
||||
let selfPeer = context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(context.account.peerId)
|
||||
}
|
||||
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta in
|
||||
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta, self.travelTimesPromise.get())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta, travelTimes in
|
||||
if let strongSelf = self, let location = getLocation(from: subject) {
|
||||
var entries: [LocationViewEntry] = []
|
||||
var annotations: [LocationPinAnnotation] = []
|
||||
@ -323,7 +352,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
var proximityNotificationRadius: Int32?
|
||||
var index: Int = 0
|
||||
|
||||
var isLocationView = false
|
||||
if location.liveBroadcastingTimeout == nil {
|
||||
isLocationView = true
|
||||
|
||||
let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
|
||||
let distance = userLocation.flatMap { subjectLocation.distance(from: $0) }
|
||||
|
||||
@ -414,11 +446,45 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
|
||||
let distance = userLocation.flatMap { subjectLocation.distance(from: $0) }
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if message.localTags.contains(.OutgoingLiveLocation), let selfPeer = selfPeer {
|
||||
userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: true, heading: location.heading)
|
||||
} else {
|
||||
var drivingTime: Double?
|
||||
var transitTime: Double?
|
||||
var walkingTime: Double?
|
||||
if !isLocationView {
|
||||
if let (previousTimestamp, maybeDrivingTime, maybeTransitTime, maybeWalkingTime) = travelTimes[message.id] {
|
||||
drivingTime = maybeDrivingTime
|
||||
transitTime = maybeTransitTime
|
||||
walkingTime = maybeWalkingTime
|
||||
|
||||
if timestamp > previousTimestamp + 60.0 {
|
||||
strongSelf.travelDisposables.add(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)).start(next: { [weak self] drivingTime, transitTime, walkingTime in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let timestamp = CACurrentMediaTime()
|
||||
var travelTimes = strongSelf.travelTimes
|
||||
travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime)
|
||||
strongSelf.travelTimes = travelTimes
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
strongSelf.travelDisposables.add(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)).start(next: { [weak self] drivingTime, transitTime, walkingTime in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let timestamp = CACurrentMediaTime()
|
||||
var travelTimes = strongSelf.travelTimes
|
||||
travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime)
|
||||
strongSelf.travelTimes = travelTimes
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: message.author?.id == context.account.peerId, heading: location.heading))
|
||||
entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, index))
|
||||
entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, index))
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
@ -441,8 +507,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
let previousState = previousState.swap(state)
|
||||
|
||||
let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction)
|
||||
let previousHadTravelTimes = previousHadTravelTimes.swap(!travelTimes.isEmpty)
|
||||
|
||||
let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction, gotTravelTimes: !travelTimes.isEmpty && !previousHadTravelTimes)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
|
||||
strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: false)
|
||||
@ -581,7 +648,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
|
||||
self.travelDisposables.dispose()
|
||||
self.locationManager.manager.stopUpdatingHeading()
|
||||
}
|
||||
|
||||
@ -633,17 +700,20 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
if !self.initialized, transition.insertions.count > 0 {
|
||||
if (!self.initialized && transition.insertions.count > 0) || transition.gotTravelTimes {
|
||||
var index: Int = 0
|
||||
var offset: CGFloat = 0.0
|
||||
if transition.insertions.count > 2 {
|
||||
if transition.gotTravelTimes {
|
||||
index = 1
|
||||
offset = 0.0
|
||||
} else if transition.insertions.count > 2 {
|
||||
index = 2
|
||||
offset = 40.0
|
||||
} else if transition.insertions.count == 2 {
|
||||
index = 1
|
||||
}
|
||||
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(offset), animated: false, curve: .Default(duration: nil), directionHint: .Up)
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(offset), animated: transition.gotTravelTimes, curve: .Default(duration: 0.3), directionHint: .Up)
|
||||
self.initialized = true
|
||||
} else {
|
||||
scrollToItem = nil
|
||||
|
@ -1,7 +1,7 @@
|
||||
#import <NetworkLogging/NetworkLogging.h>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MtProtoKit/MtLogging.h>
|
||||
#import <MtProtoKit/MTLogging.h>
|
||||
|
||||
static void (*bridgingTrace)(NSString *, NSString *);
|
||||
void setBridgingTraceFunction(void (*f)(NSString *, NSString *)) {
|
||||
|
@ -54,12 +54,12 @@ public final class OpenInActionSheetController: ActionSheetController {
|
||||
switch action {
|
||||
case let .openUrl(url):
|
||||
openUrl(url)
|
||||
case let .openLocation(latitude, longitude, withDirections):
|
||||
case let .openLocation(latitude, longitude, directions):
|
||||
let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:])
|
||||
let mapItem = MKMapItem(placemark: placemark)
|
||||
|
||||
if withDirections {
|
||||
let options = [ MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving ]
|
||||
if let directions = directions {
|
||||
let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ]
|
||||
MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options)
|
||||
} else {
|
||||
mapItem.openInMaps(launchOptions: nil)
|
||||
|
@ -8,7 +8,35 @@ import UrlEscaping
|
||||
|
||||
public enum OpenInItem {
|
||||
case url(url: String)
|
||||
case location(location: TelegramMediaMap, withDirections: Bool)
|
||||
case location(location: TelegramMediaMap, directions: OpenInLocationDirections?)
|
||||
}
|
||||
|
||||
public enum OpenInLocationDirections: Equatable {
|
||||
case walking
|
||||
case driving
|
||||
case transit
|
||||
|
||||
var transportType: MKDirectionsTransportType {
|
||||
switch self {
|
||||
case .walking:
|
||||
return .walking
|
||||
case .transit:
|
||||
return .transit
|
||||
case .driving:
|
||||
return .automobile
|
||||
}
|
||||
}
|
||||
|
||||
public var launchOptions: String {
|
||||
switch self {
|
||||
case .walking:
|
||||
return MKLaunchOptionsDirectionsModeWalking
|
||||
case .transit:
|
||||
return MKLaunchOptionsDirectionsModeTransit
|
||||
case .driving:
|
||||
return MKLaunchOptionsDirectionsModeDriving
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OpenInApplication: Equatable {
|
||||
@ -20,7 +48,7 @@ public enum OpenInApplication: Equatable {
|
||||
public enum OpenInAction {
|
||||
case none
|
||||
case openUrl(url: String)
|
||||
case openLocation(latitude: Double, longitude: Double, withDirections: Bool)
|
||||
case openLocation(latitude: Double, longitude: Double, directions: OpenInLocationDirections?)
|
||||
}
|
||||
|
||||
public final class OpenInOption {
|
||||
@ -170,11 +198,11 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
options.append(OpenInOption(identifier: "alook", application: .other(title: "Alook Browser", identifier: 1261944766, scheme: "alook", store: nil), action: {
|
||||
return .openUrl(url: "alook://\(url)")
|
||||
}))
|
||||
case let .location(location, withDirections):
|
||||
case let .location(location, directions):
|
||||
let lat = location.latitude
|
||||
let lon = location.longitude
|
||||
|
||||
if !withDirections {
|
||||
if directions == nil {
|
||||
if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "foursquare" {
|
||||
options.append(OpenInOption(identifier: "foursquare", application: .other(title: "Foursquare", identifier: 306934924, scheme: "foursquare", store: nil), action: {
|
||||
return .openUrl(url: "foursquare://venues/\(venueId)")
|
||||
@ -183,13 +211,22 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
}
|
||||
|
||||
options.append(OpenInOption(identifier: "appleMaps", application: .maps, action: {
|
||||
return .openLocation(latitude: lat, longitude: lon, withDirections: withDirections)
|
||||
return .openLocation(latitude: lat, longitude: lon, directions: directions)
|
||||
}))
|
||||
|
||||
options.append(OpenInOption(identifier: "googleMaps", application: .other(title: "Google Maps", identifier: 585027354, scheme: "comgooglemaps-x-callback", store: nil), action: {
|
||||
let coordinates = "\(lat),\(lon)"
|
||||
if withDirections {
|
||||
return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=driving&x-success=telegram://?resume=true&x-source=Telegram")
|
||||
if let directions = directions {
|
||||
let directionsMode: String
|
||||
switch directions {
|
||||
case .walking:
|
||||
directionsMode = "walking"
|
||||
case .driving:
|
||||
directionsMode = "driving"
|
||||
case .transit:
|
||||
directionsMode = "transit"
|
||||
}
|
||||
return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=\(directionsMode)&x-success=telegram://?resume=true&x-source=Telegram")
|
||||
} else {
|
||||
if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "gplaces" {
|
||||
return .openUrl(url: "https://www.google.com/maps/search/?api=1&query=\(venue.address ?? "")&query_place_id=\(venueId)")
|
||||
@ -200,7 +237,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
}))
|
||||
|
||||
options.append(OpenInOption(identifier: "yandexMaps", application: .other(title: "Yandex.Maps", identifier: 313877526, scheme: "yandexmaps", store: nil), action: {
|
||||
if withDirections {
|
||||
if let _ = directions {
|
||||
return .openUrl(url: "yandexmaps://build_route_on_map?lat_to=\(lat)&lon_to=\(lon)")
|
||||
} else {
|
||||
return .openUrl(url: "yandexmaps://maps.yandex.ru/?pt=\(lon),\(lat)&z=16")
|
||||
@ -227,7 +264,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
return .openUrl(url: "lyft://ridetype?id=lyft&destination[latitude]=\(lat)&destination[longitude]=\(lon)")
|
||||
}))
|
||||
|
||||
if withDirections {
|
||||
if let _ = directions {
|
||||
options.append(OpenInOption(identifier: "citymapper", application: .other(title: "Citymapper", identifier: 469463298, scheme: "citymapper", store: nil), action: {
|
||||
let endName: String
|
||||
let endAddress: String
|
||||
@ -251,7 +288,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
|
||||
options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: {
|
||||
let coordinates = "\(lon),\(lat)"
|
||||
if withDirections {
|
||||
if let _ = directions {
|
||||
return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go")
|
||||
} else {
|
||||
return .openUrl(url: "dgis://2gis.ru/geo/\(coordinates)")
|
||||
@ -259,7 +296,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
}))
|
||||
|
||||
options.append(OpenInOption(identifier: "moovit", application: .other(title: "Moovit", identifier: 498477945, scheme: "moovit", store: nil), action: {
|
||||
if withDirections {
|
||||
if let _ = directions {
|
||||
let destName: String
|
||||
if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 {
|
||||
destName = title
|
||||
@ -272,7 +309,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
}
|
||||
}))
|
||||
|
||||
if !withDirections {
|
||||
if directions == nil {
|
||||
options.append(OpenInOption(identifier: "hereMaps", application: .other(title: "HERE Maps", identifier: 955837609, scheme: "here-location", store: nil), action: {
|
||||
return .openUrl(url: "here-location://\(lat),\(lon)")
|
||||
}))
|
||||
@ -280,7 +317,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
|
||||
options.append(OpenInOption(identifier: "waze", application: .other(title: "Waze", identifier: 323229106, scheme: "waze", store: nil), action: {
|
||||
let url = "waze://?ll=\(lat),\(lon)"
|
||||
if withDirections {
|
||||
if let _ = directions {
|
||||
return .openUrl(url: url.appending("&navigate=yes"))
|
||||
} else {
|
||||
return .openUrl(url: url)
|
||||
|
@ -1,6 +1,6 @@
|
||||
#import <ShareItemsImpl/TGItemProviderSignals.h>
|
||||
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
#import <MtProtoKit/MtProtoKit.h>
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
@ -1,6 +1,6 @@
|
||||
#import <ShareItemsImpl/TGShareLocationSignals.h>
|
||||
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
#import <MtProtoKit/MtProtoKit.h>
|
||||
|
||||
NSString *const TGShareAppleMapsHost = @"maps.apple.com";
|
||||
NSString *const TGShareAppleMapsPath = @"/maps";
|
||||
|
@ -23,6 +23,7 @@ public enum SolidRoundedButtonFont {
|
||||
public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
private var theme: SolidRoundedButtonTheme
|
||||
private var font: SolidRoundedButtonFont
|
||||
private var fontSize: CGFloat
|
||||
|
||||
private let buttonBackgroundNode: ASDisplayNode
|
||||
private let buttonGlossNode: SolidRoundedButtonGlossNode
|
||||
@ -53,9 +54,10 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
||||
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
||||
self.theme = theme
|
||||
self.font = font
|
||||
self.fontSize = fontSize
|
||||
self.buttonHeight = height
|
||||
self.buttonCornerRadius = cornerRadius
|
||||
self.title = title
|
||||
@ -128,7 +130,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
|
||||
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
|
||||
self.buttonGlossNode.color = theme.foregroundColor
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(17.0) : Font.regular(17.0), textColor: theme.foregroundColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor)
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
|
||||
|
||||
if let width = self.validLayout {
|
||||
@ -150,7 +152,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
||||
|
||||
if self.title != self.titleNode.attributedText?.string {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(17.0) : Font.regular(17.0), textColor: self.theme.foregroundColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.theme.foregroundColor)
|
||||
}
|
||||
|
||||
let iconSize = self.iconNode.image?.size ?? CGSize()
|
||||
|
@ -893,6 +893,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
var results = self.results
|
||||
results.removeAll(where: { $0.peer.peerId == peerId})
|
||||
self.results = results
|
||||
self.count = max(0, self.count - 1)
|
||||
self.updateState()
|
||||
self.updateCache()
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ public func dateFillNeedsBlur(theme: PresentationTheme, wallpaper: TelegramWallp
|
||||
|
||||
public let defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
|
||||
public let defaultPresentationTheme = makeDefaultDayPresentationTheme(serviceBackgroundColor: defaultServiceBackgroundColor, day: false, preview: false)
|
||||
public let defaultDayAccentColor = UIColor(rgb: 0x007ee5)
|
||||
public let defaultDayAccentColor = UIColor(rgb: 0x007aff)
|
||||
|
||||
public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, outgoingAccentColor: UIColor?, backgroundColors: [UInt32], bubbleColors: [UInt32], animateBubbleColors: Bool?, wallpaper forcedWallpaper: TelegramWallpaper? = nil, serviceBackgroundColor: UIColor?) -> PresentationTheme {
|
||||
if (theme.referenceTheme != .day && theme.referenceTheme != .dayClassic) {
|
||||
@ -346,7 +346,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
let intro = PresentationThemeIntro(
|
||||
statusBarStyle: .black,
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
accentTextColor: UIColor(rgb: 0x007ee5),
|
||||
accentTextColor: defaultDayAccentColor,
|
||||
disabledTextColor: UIColor(rgb: 0xd0d0d0),
|
||||
startButtonColor: UIColor(rgb: 0x2ca5e0),
|
||||
dotColor: UIColor(rgb: 0xd9d9d9)
|
||||
@ -358,12 +358,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
)
|
||||
|
||||
let rootNavigationBar = PresentationThemeRootNavigationBar(
|
||||
buttonColor: UIColor(rgb: 0x007ee5),
|
||||
buttonColor: defaultDayAccentColor,
|
||||
disabledButtonColor: UIColor(rgb: 0xd0d0d0),
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
secondaryTextColor: UIColor(rgb: 0x787878),
|
||||
controlColor: UIColor(rgb: 0x7e8791),
|
||||
accentTextColor: UIColor(rgb: 0x007ee5),
|
||||
accentTextColor: defaultDayAccentColor,
|
||||
blurredBackgroundColor: UIColor(rgb: 0xf2f2f2, alpha: 0.9),
|
||||
opaqueBackgroundColor: UIColor(rgb: 0xf7f7f7).mixedWith(.white, alpha: 0.14),
|
||||
separatorColor: UIColor(rgb: 0xc8c7cc),
|
||||
@ -382,9 +382,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
backgroundColor: rootNavigationBar.blurredBackgroundColor,
|
||||
separatorColor: UIColor(rgb: 0xa3a3a3),
|
||||
iconColor: UIColor(rgb: 0x959595),
|
||||
selectedIconColor: UIColor(rgb: 0x007ee5),
|
||||
selectedIconColor: defaultDayAccentColor,
|
||||
textColor: UIColor(rgb: 0x959595),
|
||||
selectedTextColor: UIColor(rgb: 0x007ee5),
|
||||
selectedTextColor: defaultDayAccentColor,
|
||||
badgeBackgroundColor: UIColor(rgb: 0xff3b30),
|
||||
badgeStrokeColor: UIColor(rgb: 0xff3b30),
|
||||
badgeTextColor: UIColor(rgb: 0xffffff)
|
||||
@ -392,7 +392,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
|
||||
let navigationSearchBar = PresentationThemeNavigationSearchBar(
|
||||
backgroundColor: UIColor(rgb: 0xffffff),
|
||||
accentColor: UIColor(rgb: 0x007ee5),
|
||||
accentColor: defaultDayAccentColor,
|
||||
inputFillColor: UIColor(rgb: 0x000000, alpha: 0.06),
|
||||
inputTextColor: UIColor(rgb: 0x000000),
|
||||
inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93),
|
||||
@ -423,7 +423,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
itemPrimaryTextColor: UIColor(rgb: 0x000000),
|
||||
itemSecondaryTextColor: UIColor(rgb: 0x8e8e93),
|
||||
itemDisabledTextColor: UIColor(rgb: 0x8e8e93),
|
||||
itemAccentColor: UIColor(rgb: 0x007ee5),
|
||||
itemAccentColor: defaultDayAccentColor,
|
||||
itemHighlightedColor: UIColor(rgb: 0x00b12c),
|
||||
itemDestructiveColor: UIColor(rgb: 0xff3b30),
|
||||
itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce),
|
||||
@ -443,12 +443,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xf09a37), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3824), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
constructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x00c900), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
accent: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x007ee5), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
accent: PresentationThemeFillForeground(fillColor: defaultDayAccentColor, foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff9500), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: UIColor(rgb: 0xffffff))
|
||||
),
|
||||
itemCheckColors: PresentationThemeFillStrokeForeground(
|
||||
fillColor: UIColor(rgb: 0x007ee5),
|
||||
fillColor: defaultDayAccentColor,
|
||||
strokeColor: UIColor(rgb: 0xc7c7cc),
|
||||
foregroundColor: UIColor(rgb: 0xffffff)
|
||||
),
|
||||
@ -471,7 +471,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
scrollIndicatorColor: UIColor(white: 0.0, alpha: 0.3),
|
||||
pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7),
|
||||
inputClearButtonColor: UIColor(rgb: 0xcccccc),
|
||||
itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)),
|
||||
itemBarChart: PresentationThemeItemBarChart(color1: defaultDayAccentColor, color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)),
|
||||
itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb)),
|
||||
paymentOption: PresentationThemeList.PaymentOption(
|
||||
inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1),
|
||||
@ -495,12 +495,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
messageTextColor: UIColor(rgb: 0x8e8e93),
|
||||
messageHighlightedTextColor: UIColor(rgb: 0x000000),
|
||||
messageDraftTextColor: UIColor(rgb: 0xdd4b39),
|
||||
checkmarkColor: day ? UIColor(rgb: 0x007ee5) : UIColor(rgb: 0x21c004),
|
||||
checkmarkColor: day ? defaultDayAccentColor : UIColor(rgb: 0x21c004),
|
||||
pendingIndicatorColor: UIColor(rgb: 0x8e8e93),
|
||||
failedFillColor: UIColor(rgb: 0xff3b30),
|
||||
failedForegroundColor: UIColor(rgb: 0xffffff),
|
||||
muteIconColor: UIColor(rgb: 0xa7a7ad),
|
||||
unreadBadgeActiveBackgroundColor: UIColor(rgb: 0x007ee5),
|
||||
unreadBadgeActiveBackgroundColor: defaultDayAccentColor,
|
||||
unreadBadgeActiveTextColor: UIColor(rgb: 0xffffff),
|
||||
unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb),
|
||||
unreadBadgeInactiveTextColor: UIColor(rgb: 0xffffff),
|
||||
@ -509,7 +509,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
regularSearchBarColor: UIColor(rgb: 0xe9e9e9),
|
||||
sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7),
|
||||
sectionHeaderTextColor: UIColor(rgb: 0x8e8e93),
|
||||
verifiedIconFillColor: UIColor(rgb: 0x007ee5),
|
||||
verifiedIconFillColor: defaultDayAccentColor,
|
||||
verifiedIconForegroundColor: UIColor(rgb: 0xffffff),
|
||||
secretIconColor: UIColor(rgb: 0x00b12c),
|
||||
pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
@ -530,13 +530,13 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
linkTextColor: UIColor(rgb: 0x004bad),
|
||||
linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3),
|
||||
linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3),
|
||||
scamColor: UIColor(rgb: 0xff3b30),
|
||||
textHighlightColor: UIColor(rgb: 0xffe438),
|
||||
accentTextColor: UIColor(rgb: 0x007ee5),
|
||||
accentControlColor: UIColor(rgb: 0x007ee5),
|
||||
accentTextColor: defaultDayAccentColor,
|
||||
accentControlColor: defaultDayAccentColor,
|
||||
accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
mediaActiveControlColor: UIColor(rgb: 0x007ee5),
|
||||
mediaActiveControlColor: defaultDayAccentColor,
|
||||
mediaInactiveControlColor: UIColor(rgb: 0xcacaca),
|
||||
mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff),
|
||||
pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
@ -544,15 +544,15 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
fileDescriptionColor: UIColor(rgb: 0x999999),
|
||||
fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
mediaPlaceholderColor: UIColor(rgb: 0xe8ecf0),
|
||||
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.08), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x2dba45), barNegative: UIColor(rgb: 0xFE3824)),
|
||||
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: defaultDayAccentColor, highlight: defaultDayAccentColor.withAlphaComponent(0.08), separator: UIColor(rgb: 0xc8c7cc), bar: defaultDayAccentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x2dba45), barNegative: UIColor(rgb: 0xFE3824)),
|
||||
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear),
|
||||
actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0x007ee5)),
|
||||
actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.2), textSelectionKnobColor: defaultDayAccentColor),
|
||||
outgoing: PresentationThemePartedColors(
|
||||
bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil)),
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8),
|
||||
linkTextColor: UIColor(rgb: 0x004bad),
|
||||
linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3),
|
||||
linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3),
|
||||
scamColor: UIColor(rgb: 0xff3b30),
|
||||
textHighlightColor: UIColor(rgb: 0xffe438),
|
||||
accentTextColor: UIColor(rgb: 0x00a700),
|
||||
@ -582,7 +582,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear),
|
||||
shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)),
|
||||
mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6),
|
||||
stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.25)),
|
||||
@ -595,28 +595,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
linkTextColor: UIColor(rgb: 0x004bad),
|
||||
linkHighlightColor: UIColor(rgb: 0x007ee5, alpha: 0.3),
|
||||
linkHighlightColor: defaultDayAccentColor.withAlphaComponent(0.3),
|
||||
scamColor: UIColor(rgb: 0xff3b30),
|
||||
textHighlightColor: UIColor(rgb: 0xffc738),
|
||||
accentTextColor: UIColor(rgb: 0x007ee5),
|
||||
accentControlColor: UIColor(rgb: 0x007ee5),
|
||||
accentTextColor: defaultDayAccentColor,
|
||||
accentControlColor: defaultDayAccentColor,
|
||||
accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
mediaActiveControlColor: UIColor(rgb: 0x007ee5),
|
||||
mediaActiveControlColor: defaultDayAccentColor,
|
||||
mediaInactiveControlColor: UIColor(rgb: 0xcacaca),
|
||||
mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff),
|
||||
pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
fileTitleColor: UIColor(rgb: 0x007ee5),
|
||||
fileTitleColor: defaultDayAccentColor,
|
||||
fileDescriptionColor: UIColor(rgb: 0x999999),
|
||||
fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6),
|
||||
mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.95),
|
||||
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.12), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)),
|
||||
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: defaultDayAccentColor, highlight: defaultDayAccentColor.withAlphaComponent(0.12), separator: UIColor(rgb: 0xc8c7cc), bar: defaultDayAccentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)),
|
||||
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
|
||||
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)),
|
||||
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)),
|
||||
textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.3),
|
||||
textSelectionKnobColor: UIColor(rgb: 0x007ee5)),
|
||||
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: defaultDayAccentColor),
|
||||
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor),
|
||||
textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.3),
|
||||
textSelectionKnobColor: defaultDayAccentColor),
|
||||
outgoing: PresentationThemePartedColors(
|
||||
bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), UIColor(rgb: 0x007ee5)], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), UIColor(rgb: 0x007ee5)], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)),
|
||||
bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)),
|
||||
primaryTextColor: UIColor(rgb: 0xffffff),
|
||||
secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65),
|
||||
linkTextColor: UIColor(rgb: 0xffffff),
|
||||
@ -636,8 +636,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
mediaPlaceholderColor: UIColor(rgb: 0x0077d9),
|
||||
polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff, alpha: 0.65), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.65), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)),
|
||||
actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
|
||||
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)),
|
||||
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)),
|
||||
actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: defaultDayAccentColor),
|
||||
actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor),
|
||||
textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2),
|
||||
textSelectionKnobColor: UIColor(rgb: 0xffffff)),
|
||||
freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil)),
|
||||
@ -648,9 +648,9 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff),
|
||||
shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)),
|
||||
shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0xe5e5ea)),
|
||||
shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)),
|
||||
shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor),
|
||||
mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)),
|
||||
mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6),
|
||||
stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor.withAlphaComponent(0.3), withoutWallpaper: UIColor(rgb: 0xf7f7f7)),
|
||||
@ -674,8 +674,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
)
|
||||
|
||||
let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl(
|
||||
buttonColor: UIColor(rgb: 0x007ee5),
|
||||
micLevelColor: UIColor(rgb: 0x007ee5, alpha: 0.2),
|
||||
buttonColor: defaultDayAccentColor,
|
||||
micLevelColor: defaultDayAccentColor.withAlphaComponent(0.2),
|
||||
activeIconColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
|
||||
@ -683,7 +683,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
panelBackgroundColor: rootNavigationBar.blurredBackgroundColor,
|
||||
panelBackgroundColorNoWallpaper: rootNavigationBar.blurredBackgroundColor,
|
||||
panelSeparatorColor: UIColor(rgb: 0xb2b2b2),
|
||||
panelControlAccentColor: UIColor(rgb: 0x007ee5),
|
||||
panelControlAccentColor: defaultDayAccentColor,
|
||||
panelControlColor: UIColor(rgb: 0x858e99),
|
||||
panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5),
|
||||
panelControlDestructiveColor: UIColor(rgb: 0xff3b30),
|
||||
@ -692,7 +692,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
inputPlaceholderColor: UIColor(rgb: 0xbebec0),
|
||||
inputTextColor: UIColor(rgb: 0x000000),
|
||||
inputControlColor: UIColor(rgb: 0xa0a7b0),
|
||||
actionControlFillColor: UIColor(rgb: 0x007ee5),
|
||||
actionControlFillColor: defaultDayAccentColor,
|
||||
actionControlForegroundColor: UIColor(rgb: 0xffffff),
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
secondaryTextColor: UIColor(rgb: 0x8e8e93),
|
||||
@ -727,8 +727,8 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
fillColor: UIColor(rgb: 0xf7f7f7),
|
||||
strokeColor: UIColor(rgb: 0xc8c7cc),
|
||||
foregroundColor: UIColor(rgb: 0x88888d),
|
||||
badgeBackgroundColor: UIColor(rgb: 0x007ee5),
|
||||
badgeStrokeColor: UIColor(rgb: 0x007ee5),
|
||||
badgeBackgroundColor: defaultDayAccentColor,
|
||||
badgeStrokeColor: defaultDayAccentColor,
|
||||
badgeTextColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
|
||||
@ -753,12 +753,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0),
|
||||
itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7),
|
||||
opaqueItemSeparatorColor: UIColor(white: 0.9, alpha: 1.0),
|
||||
standardActionTextColor: UIColor(rgb: 0x007ee5),
|
||||
standardActionTextColor: defaultDayAccentColor,
|
||||
destructiveActionTextColor: UIColor(rgb: 0xff3b30),
|
||||
disabledActionTextColor: UIColor(rgb: 0xb3b3b3),
|
||||
primaryTextColor: UIColor(rgb: 0x000000),
|
||||
secondaryTextColor: UIColor(rgb: 0x5e5e5e),
|
||||
controlAccentColor: UIColor(rgb: 0x007ee5),
|
||||
secondaryTextColor: UIColor(rgb: 0x8e8e93),
|
||||
controlAccentColor: defaultDayAccentColor,
|
||||
inputBackgroundColor: UIColor(rgb: 0xe9e9e9),
|
||||
inputHollowBackgroundColor: UIColor(rgb: 0xffffff),
|
||||
inputBorderColor: UIColor(rgb: 0xe4e4e6),
|
||||
@ -778,7 +778,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio
|
||||
primaryColor: UIColor(rgb: 0x000000),
|
||||
secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8),
|
||||
destructiveColor: UIColor(rgb: 0xff3b30),
|
||||
badgeFillColor: UIColor(rgb: 0x007ee5),
|
||||
badgeFillColor: defaultDayAccentColor,
|
||||
badgeForegroundColor: UIColor(rgb: 0xffffff),
|
||||
badgeInactiveFillColor: UIColor(rgb: 0xb6b6bb),
|
||||
badgeInactiveForegroundColor: UIColor(rgb: 0xffffff),
|
||||
|
21
submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
21
submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
21
submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
108
submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js
Normal file
108
submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js
Normal 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();
|
||||
}
|
@ -484,6 +484,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
private var nextChannelToReadDisposable: Disposable?
|
||||
|
||||
private var inviteRequestsContext: PeerInvitationImportersContext?
|
||||
private var inviteRequestsDisposable = MetaDisposable()
|
||||
|
||||
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil) {
|
||||
let _ = ChatControllerCount.modify { value in
|
||||
return value + 1
|
||||
@ -4160,6 +4163,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.addMemberDisposable.dispose()
|
||||
self.importStateDisposable?.dispose()
|
||||
self.nextChannelToReadDisposable?.dispose()
|
||||
self.inviteRequestsDisposable.dispose()
|
||||
}
|
||||
|
||||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||||
@ -4729,6 +4733,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var callsPrivate: Bool = false
|
||||
var slowmodeState: ChatSlowmodeState?
|
||||
var activeGroupCallInfo: ChatActiveGroupCallInfo?
|
||||
var inviteRequestsPending: Int32?
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
pinnedMessageId = cachedData.pinnedMessageId
|
||||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout {
|
||||
@ -4741,6 +4746,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let activeCall = cachedData.activeCall {
|
||||
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
||||
}
|
||||
inviteRequestsPending = cachedData.inviteRequestsPending
|
||||
} else if let cachedData = cachedData as? CachedUserData {
|
||||
peerIsBlocked = cachedData.isBlocked
|
||||
callsAvailable = cachedData.voiceCallsAvailable
|
||||
@ -4751,6 +4757,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let activeCall = cachedData.activeCall {
|
||||
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
||||
}
|
||||
inviteRequestsPending = cachedData.inviteRequestsPending
|
||||
} else if let _ = cachedData as? CachedSecretChatData {
|
||||
}
|
||||
|
||||
@ -4782,7 +4789,63 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate
|
||||
|
||||
|
||||
if let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0, strongSelf.inviteRequestsContext == nil {
|
||||
let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil)
|
||||
strongSelf.inviteRequestsContext = inviteRequestsContext
|
||||
|
||||
strongSelf.inviteRequestsDisposable.set((inviteRequestsContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] requestsState in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||||
return state
|
||||
.updatedTitlePanelContext({ context in
|
||||
if requestsState.count > 0 {
|
||||
let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3))
|
||||
if !context.contains(where: {
|
||||
switch $0 {
|
||||
case .inviteRequests(peers, requestsState.count):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = context.filter { c in
|
||||
if case .inviteRequests = c {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
updatedContexts.append(.inviteRequests(peers, requestsState.count))
|
||||
return updatedContexts.sorted()
|
||||
} else {
|
||||
return context
|
||||
}
|
||||
} else {
|
||||
if let index = context.firstIndex(where: {
|
||||
switch $0 {
|
||||
case .inviteRequests:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = context
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
} else {
|
||||
return context
|
||||
}
|
||||
}
|
||||
})
|
||||
.updatedSlowmodeState(slowmodeState)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
|
||||
return state
|
||||
@ -7269,6 +7332,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return $0.updatedShowCommands(f($0.showCommands))
|
||||
})
|
||||
}
|
||||
}, openInviteRequests: { [weak self] in
|
||||
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||
let controller = inviteRequestsController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, existingContext: strongSelf.inviteRequestsContext)
|
||||
controller.navigationPresentation = .modal
|
||||
strongSelf.push(controller)
|
||||
}
|
||||
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
|
||||
|
||||
do {
|
||||
|
@ -40,7 +40,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
|
||||
break loop
|
||||
}
|
||||
}
|
||||
case .chatInfo, .requestInProgress, .toastAlert:
|
||||
case .chatInfo, .requestInProgress, .toastAlert, .inviteRequests:
|
||||
selectedContext = context
|
||||
break loop
|
||||
}
|
||||
@ -119,6 +119,16 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
return panel
|
||||
}
|
||||
case let .inviteRequests(peers, count):
|
||||
if let currentPanel = currentPanel as? ChatInviteRequestsTitlePanelNode {
|
||||
currentPanel.update(peers: peers, count: count)
|
||||
return currentPanel
|
||||
} else {
|
||||
let panel = ChatInviteRequestsTitlePanelNode(context: context)
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
panel.update(peers: peers, count: count)
|
||||
return panel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -132,6 +132,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
let presentInviteMembers: () -> Void
|
||||
let presentGigagroupHelp: () -> Void
|
||||
let updateShowCommands: ((Bool) -> Bool) -> Void
|
||||
let openInviteRequests: () -> Void
|
||||
let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
|
||||
init(
|
||||
@ -218,6 +219,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
presentGigagroupHelp: @escaping () -> Void,
|
||||
editMessageMedia: @escaping (MessageId, Bool) -> Void,
|
||||
updateShowCommands: @escaping ((Bool) -> Bool) -> Void,
|
||||
openInviteRequests: @escaping () -> Void,
|
||||
statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
) {
|
||||
self.setupReplyMessage = setupReplyMessage
|
||||
@ -303,6 +305,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
self.presentInviteMembers = presentInviteMembers
|
||||
self.presentGigagroupHelp = presentGigagroupHelp
|
||||
self.updateShowCommands = updateShowCommands
|
||||
self.openInviteRequests = openInviteRequests
|
||||
self.statuses = statuses
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ enum ChatTitlePanelContext: Equatable, Comparable {
|
||||
case chatInfo
|
||||
case requestInProgress
|
||||
case toastAlert(String)
|
||||
case inviteRequests([EnginePeer], Int32)
|
||||
|
||||
private var index: Int {
|
||||
switch self {
|
||||
@ -93,6 +94,8 @@ enum ChatTitlePanelContext: Equatable, Comparable {
|
||||
return 2
|
||||
case .toastAlert:
|
||||
return 3
|
||||
case .inviteRequests:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +144,9 @@ final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, presentInviteMembers: {
|
||||
}, presentGigagroupHelp: {
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, updateShowCommands: { _ in }, statuses: nil)
|
||||
}, updateShowCommands: { _ in
|
||||
}, openInviteRequests: {
|
||||
}, statuses: nil)
|
||||
|
||||
self.navigationItem.titleView = self.titleView
|
||||
|
||||
|
@ -181,6 +181,7 @@ final class PeerInfoScreenData {
|
||||
let encryptionKeyFingerprint: SecretChatKeyFingerprint?
|
||||
let globalSettings: TelegramGlobalSettings?
|
||||
let invitations: PeerExportedInvitationsState?
|
||||
let requests: PeerInvitationImportersState?
|
||||
|
||||
init(
|
||||
peer: Peer?,
|
||||
@ -195,7 +196,8 @@ final class PeerInfoScreenData {
|
||||
members: PeerInfoMembersData?,
|
||||
encryptionKeyFingerprint: SecretChatKeyFingerprint?,
|
||||
globalSettings: TelegramGlobalSettings?,
|
||||
invitations: PeerExportedInvitationsState?
|
||||
invitations: PeerExportedInvitationsState?,
|
||||
requests: PeerInvitationImportersState?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.cachedData = cachedData
|
||||
@ -210,6 +212,7 @@ final class PeerInfoScreenData {
|
||||
self.encryptionKeyFingerprint = encryptionKeyFingerprint
|
||||
self.globalSettings = globalSettings
|
||||
self.invitations = invitations
|
||||
self.requests = requests
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,7 +436,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: globalSettings,
|
||||
invitations: nil
|
||||
invitations: nil,
|
||||
requests: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -456,7 +460,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: nil,
|
||||
invitations: nil
|
||||
invitations: nil,
|
||||
requests: nil
|
||||
))
|
||||
case let .user(userPeerId, secretChatId, kind):
|
||||
let groupsInCommon: GroupsInCommonContext?
|
||||
@ -596,7 +601,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: encryptionKeyFingerprint,
|
||||
globalSettings: nil,
|
||||
invitations: nil
|
||||
invitations: nil,
|
||||
requests: nil
|
||||
)
|
||||
}
|
||||
case .channel:
|
||||
@ -620,15 +626,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
|
||||
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
|
||||
|
||||
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
|
||||
let requestsStatePromise = Promise<PeerInvitationImportersState?>(nil)
|
||||
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId),
|
||||
context.account.postbox.combinedView(keys: combinedKeys),
|
||||
status,
|
||||
invitationsContextPromise.get(),
|
||||
invitationsStatePromise.get()
|
||||
invitationsStatePromise.get(),
|
||||
requestsContextPromise.get(),
|
||||
requestsStatePromise.get()
|
||||
)
|
||||
|> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations -> PeerInfoScreenData in
|
||||
|> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations, currentRequestsContext, requests -> PeerInfoScreenData in
|
||||
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
|
||||
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
|
||||
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
|
||||
@ -641,17 +652,25 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
discussionPeer = peer
|
||||
}
|
||||
|
||||
var canManageInvitations = false
|
||||
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let _ = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
if currentInvitationsContext == nil {
|
||||
var canManageInvitations = false
|
||||
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let _ = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
if canManageInvitations {
|
||||
let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true)
|
||||
invitationsContextPromise.set(.single(invitationsContext))
|
||||
invitationsStatePromise.set(invitationsContext.state |> map(Optional.init))
|
||||
}
|
||||
}
|
||||
|
||||
if currentRequestsContext == nil {
|
||||
if canManageInvitations {
|
||||
let requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil)
|
||||
requestsContextPromise.set(.single(requestsContext))
|
||||
requestsStatePromise.set(requestsContext.state |> map(Optional.init))
|
||||
}
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
peer: peerView.peers[peerId],
|
||||
@ -666,7 +685,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: nil,
|
||||
invitations: invitations
|
||||
invitations: invitations,
|
||||
requests: requests
|
||||
)
|
||||
}
|
||||
case let .group(groupId):
|
||||
@ -767,6 +787,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
|
||||
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
|
||||
|
||||
let requestsContextPromise = Promise<PeerInvitationImportersContext?>(nil)
|
||||
let requestsStatePromise = Promise<PeerInvitationImportersState?>(nil)
|
||||
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
context.account.viewTracker.peerView(groupId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: groupId),
|
||||
@ -774,9 +797,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
status,
|
||||
membersData,
|
||||
invitationsContextPromise.get(),
|
||||
invitationsStatePromise.get()
|
||||
invitationsStatePromise.get(),
|
||||
requestsContextPromise.get(),
|
||||
requestsStatePromise.get()
|
||||
)
|
||||
|> map { peerView, availablePanes, combinedView, status, membersData, currentInvitationsContext, invitations -> PeerInfoScreenData in
|
||||
|> map { peerView, availablePanes, combinedView, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests -> PeerInfoScreenData in
|
||||
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
|
||||
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
|
||||
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
|
||||
@ -798,23 +823,31 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
}
|
||||
}
|
||||
|
||||
if currentInvitationsContext == nil {
|
||||
var canManageInvitations = false
|
||||
if let group = peerViewMainPeer(peerView) as? TelegramGroup {
|
||||
if case .creator = group.role {
|
||||
canManageInvitations = true
|
||||
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
} else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
|
||||
var canManageInvitations = false
|
||||
if let group = peerViewMainPeer(peerView) as? TelegramGroup {
|
||||
if case .creator = group.role {
|
||||
canManageInvitations = true
|
||||
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
} else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
if currentInvitationsContext == nil {
|
||||
if canManageInvitations {
|
||||
let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true)
|
||||
invitationsContextPromise.set(.single(invitationsContext))
|
||||
invitationsStatePromise.set(invitationsContext.state |> map(Optional.init))
|
||||
}
|
||||
}
|
||||
|
||||
if currentRequestsContext == nil {
|
||||
if canManageInvitations {
|
||||
let requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, invite: nil)
|
||||
requestsContextPromise.set(.single(requestsContext))
|
||||
requestsStatePromise.set(requestsContext.state |> map(Optional.init))
|
||||
}
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
peer: peerView.peers[groupId],
|
||||
@ -829,7 +862,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
members: membersData,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: nil,
|
||||
invitations: invitations
|
||||
invitations: invitations,
|
||||
requests: requests
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -454,7 +454,9 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
||||
}, presentInviteMembers: {
|
||||
}, presentGigagroupHelp: {
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, updateShowCommands: { _ in }, statuses: nil)
|
||||
}, updateShowCommands: { _ in
|
||||
}, openInviteRequests: {
|
||||
}, statuses: nil)
|
||||
|
||||
self.selectionPanel.interfaceInteraction = interfaceInteraction
|
||||
|
||||
@ -494,6 +496,7 @@ private enum PeerInfoParticipantsSection {
|
||||
case members
|
||||
case admins
|
||||
case banned
|
||||
case memberRequests
|
||||
}
|
||||
|
||||
private enum PeerInfoMemberAction {
|
||||
@ -1034,7 +1037,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
let ItemAbout = 2
|
||||
let ItemAdmins = 3
|
||||
let ItemMembers = 4
|
||||
let ItemBanned = 5
|
||||
let ItemMemberRequests = 5
|
||||
let ItemBanned = 6
|
||||
let ItemLocationHeader = 7
|
||||
let ItemLocation = 8
|
||||
|
||||
@ -1105,6 +1109,13 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
|
||||
interaction.openParticipantsSection(.members)
|
||||
}))
|
||||
|
||||
if let count = data.requests?.count, count > 0 {
|
||||
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
|
||||
interaction.openParticipantsSection(.memberRequests)
|
||||
}))
|
||||
}
|
||||
|
||||
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: {
|
||||
interaction.openParticipantsSection(.banned)
|
||||
}))
|
||||
@ -4711,6 +4722,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}
|
||||
case .banned:
|
||||
self.controller?.push(channelBlacklistController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId))
|
||||
case .memberRequests:
|
||||
self.controller?.push(inviteRequestsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,7 +300,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, presentInviteMembers: {
|
||||
}, presentGigagroupHelp: {
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, updateShowCommands: { _ in }, statuses: nil)
|
||||
}, updateShowCommands: { _ in
|
||||
}, openInviteRequests: {
|
||||
}, statuses: nil)
|
||||
|
||||
self.readyValue.set(self.chatListNode.ready)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ objc_library(
|
||||
"Sources/**/*.mm",
|
||||
"Sources/**/*.h",
|
||||
"tgcalls/tgcalls/**/*.h",
|
||||
"tgcalls/tgcalls/**/*.hpp",
|
||||
"tgcalls/tgcalls/**/*.cpp",
|
||||
"tgcalls/tgcalls/**/*.mm",
|
||||
"tgcalls/tgcalls/**/*.m",
|
||||
|
1
third-party/ogg/BUILD
vendored
1
third-party/ogg/BUILD
vendored
@ -5,6 +5,7 @@ objc_library(
|
||||
module_name = "ogg",
|
||||
srcs = glob([
|
||||
"Sources/*.c",
|
||||
"Sources/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"include/ogg/*.h",
|
||||
|
1
third-party/opusfile/BUILD
vendored
1
third-party/opusfile/BUILD
vendored
@ -5,6 +5,7 @@ objc_library(
|
||||
module_name = "opusfile",
|
||||
srcs = glob([
|
||||
"Sources/*.c",
|
||||
"Sources/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"include/opusfile/*.h",
|
||||
|
56
third-party/webrtc/BUILD
vendored
56
third-party/webrtc/BUILD
vendored
@ -34,6 +34,18 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/container/internal/layout.h",
|
||||
"absl/container/internal/hashtable_debug_hooks.h",
|
||||
"absl/strings/internal/cord_internal.h",
|
||||
"absl/strings/internal/cord_rep_btree.h",
|
||||
"absl/strings/internal/cord_rep_flat.h",
|
||||
"absl/strings/internal/cord_rep_btree_reader.h",
|
||||
"absl/strings/internal/cord_rep_btree_navigator.h",
|
||||
"absl/strings/internal/cord_rep_ring.h",
|
||||
"absl/strings/internal/cordz_functions.h",
|
||||
"absl/strings/internal/cordz_info.h",
|
||||
"absl/strings/internal/cordz_handle.h",
|
||||
"absl/strings/internal/cordz_statistics.h",
|
||||
"absl/strings/internal/cordz_update_tracker.h",
|
||||
"absl/strings/internal/cordz_update_scope.h",
|
||||
"absl/strings/internal/string_constant.h",
|
||||
"absl/base/internal/inline_variable.h",
|
||||
"absl/base/internal/cycleclock.cc",
|
||||
"absl/base/internal/exponential_biased.cc",
|
||||
@ -45,6 +57,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/base/internal/spinlock_wait.cc",
|
||||
"absl/base/internal/strerror.cc",
|
||||
"absl/base/internal/sysinfo.cc",
|
||||
"absl/base/internal/thread_annotations.h",
|
||||
"absl/base/internal/thread_identity.cc",
|
||||
"absl/base/internal/throw_delegate.cc",
|
||||
"absl/base/internal/unscaledcycleclock.cc",
|
||||
@ -117,6 +130,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/strings/str_split.cc",
|
||||
"absl/strings/string_view.cc",
|
||||
"absl/strings/substitute.cc",
|
||||
"absl/synchronization/mutex.h",
|
||||
"absl/synchronization/barrier.cc",
|
||||
"absl/synchronization/blocking_counter.cc",
|
||||
"absl/synchronization/internal/create_thread_identity.cc",
|
||||
@ -188,6 +202,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/strings/internal/charconv_bigint.h",
|
||||
"absl/strings/escaping.h",
|
||||
"absl/status/status.h",
|
||||
"absl/status/internal/status_internal.h",
|
||||
"absl/strings/cord.h",
|
||||
"absl/random/internal/randen_slow.h",
|
||||
"absl/random/internal/randen_detect.h",
|
||||
@ -198,7 +213,14 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/random/internal/pool_urbg.h",
|
||||
"absl/random/internal/distribution_test_util.h",
|
||||
"absl/flags/usage.h",
|
||||
"absl/flags/commandlineflag.h",
|
||||
"absl/flags/internal/sequence_lock.h",
|
||||
"absl/flags/internal/private_handle_accessor.h",
|
||||
"absl/flags/reflection.h",
|
||||
"absl/numeric/int128.h",
|
||||
"absl/numeric/bits.h",
|
||||
"absl/numeric/internal/bits.h",
|
||||
"absl/numeric/internal/representation.h",
|
||||
"absl/flags/marshalling.h",
|
||||
"absl/flags/parse.h",
|
||||
"absl/flags/internal/commandlineflag.h",
|
||||
@ -216,6 +238,8 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/container/internal/raw_hash_set.h",
|
||||
"absl/debugging/failure_signal_handler.h",
|
||||
"absl/container/internal/hash_generator_testing.h",
|
||||
"absl/container/flat_hash_map.h",
|
||||
"absl/container/internal/hash_function_defaults.h",
|
||||
"absl/base/internal/spinlock_wait.h",
|
||||
"absl/base/log_severity.h",
|
||||
"absl/base/internal/sysinfo.h",
|
||||
@ -231,6 +255,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/time/internal/cctz/include/cctz/zone_info_source.h",
|
||||
"absl/time/time.h",
|
||||
"absl/time/clock.h",
|
||||
"absl/synchronization/internal/futex.h",
|
||||
"absl/synchronization/internal/waiter.h",
|
||||
"absl/strings/str_split.h",
|
||||
"absl/strings/internal/str_format/float_conversion.h",
|
||||
@ -245,10 +270,13 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/hash/internal/hash.h",
|
||||
"absl/random/internal/nanobenchmark.h",
|
||||
"absl/hash/internal/city.h",
|
||||
"absl/hash/internal/low_level_hash.h",
|
||||
"absl/debugging/symbolize.h",
|
||||
"absl/debugging/internal/stack_consumption.h",
|
||||
"absl/flags/internal/flag.h",
|
||||
"absl/container/internal/hashtablez_sampler.h",
|
||||
"absl/container/internal/raw_hash_map.h",
|
||||
"absl/profiling/internal/sample_recorder.h",
|
||||
"absl/base/internal/unscaledcycleclock.h",
|
||||
"absl/base/internal/thread_identity.h",
|
||||
"absl/base/internal/cycleclock.h",
|
||||
@ -293,7 +321,9 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/container/internal/have_sse.h",
|
||||
"absl/container/internal/inlined_vector.h",
|
||||
"absl/debugging/internal/stacktrace_unimplemented-inl.inc",
|
||||
"absl/debugging/internal/stacktrace_generic-inl.inc",
|
||||
"absl/debugging/symbolize_unimplemented.inc",
|
||||
"absl/debugging/symbolize_darwin.inc",
|
||||
"absl/flags/config.h",
|
||||
"absl/flags/internal/path_util.h",
|
||||
"absl/hash/hash.h",
|
||||
@ -314,6 +344,7 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/types/internal/variant.h",
|
||||
"absl/base/internal/direct_mmap.h",
|
||||
"absl/base/internal/spinlock_posix.inc",
|
||||
"absl/base/internal/dynamic_annotations.h",
|
||||
"absl/container/fixed_array.h",
|
||||
"absl/container/internal/common.h",
|
||||
"absl/container/internal/compressed_tuple.h",
|
||||
@ -328,6 +359,8 @@ absl_sources = [ "dependencies/third_party/abseil-cpp/" + x for x in [
|
||||
"absl/random/internal/wide_multiply.h",
|
||||
"absl/container/internal/hash_policy_traits.h",
|
||||
"absl/functional/internal/function_ref.h",
|
||||
"absl/functional/bind_front.h",
|
||||
"absl/functional/internal/front_binder.h",
|
||||
]]
|
||||
|
||||
webrtc_sources = [
|
||||
@ -1658,6 +1691,7 @@ webrtc_sources = [
|
||||
"call/flexfec_receive_stream.h",
|
||||
"call/flexfec_receive_stream_impl.h",
|
||||
"call/receive_time_calculator.h",
|
||||
"call/receive_stream.h",
|
||||
"call/rtp_bitrate_configurator.h",
|
||||
"call/rtp_config.h",
|
||||
"call/rtp_demuxer.h",
|
||||
@ -2297,8 +2331,11 @@ webrtc_sources = [
|
||||
"pc/media_session.h",
|
||||
"pc/media_stream.h",
|
||||
"pc/media_stream_observer.h",
|
||||
"pc/media_stream_track_proxy.h",
|
||||
"pc/media_stream_proxy.h",
|
||||
"pc/peer_connection.h",
|
||||
"pc/peer_connection_factory.h",
|
||||
"pc/proxy.h",
|
||||
"pc/remote_audio_source.h",
|
||||
"pc/rtc_stats_collector.h",
|
||||
"pc/rtc_stats_traversal.h",
|
||||
@ -2307,6 +2344,8 @@ webrtc_sources = [
|
||||
"pc/rtp_parameters_conversion.h",
|
||||
"pc/rtp_receiver.h",
|
||||
"pc/rtp_sender.h",
|
||||
"pc/rtp_receiver_proxy.h",
|
||||
"pc/rtp_sender_proxy.h",
|
||||
"pc/rtp_transceiver.h",
|
||||
"pc/rtp_transport.h",
|
||||
"pc/sctp_data_channel_transport.h",
|
||||
@ -2352,6 +2391,7 @@ webrtc_sources = [
|
||||
"modules/rtp_rtcp/source/byte_io.h",
|
||||
"modules/video_capture/video_capture.h",
|
||||
"modules/video_coding/codecs/h264/include/h264_globals.h",
|
||||
"modules/video_coding/codecs/h264/h264_color_space.h",
|
||||
"modules/video_coding/codecs/interface/common_constants.h",
|
||||
"modules/video_coding/include/video_coding.h",
|
||||
"modules/video_coding/internal_defines.h",
|
||||
@ -2409,6 +2449,14 @@ webrtc_sources = [
|
||||
"rtc_base/system/unused.h",
|
||||
"rtc_base/time_utils.h",
|
||||
"rtc_base/units/unit_base.h",
|
||||
"rtc_base/containers/flat_map.h",
|
||||
"rtc_base/containers/flat_tree.h",
|
||||
"rtc_base/containers/as_const.h",
|
||||
"rtc_base/containers/not_fn.h",
|
||||
"rtc_base/containers/invoke.h",
|
||||
"rtc_base/containers/void_t.h",
|
||||
"rtc_base/containers/flat_set.h",
|
||||
"rtc_base/containers/identity.h",
|
||||
"video/adaptation/encode_usage_resource.h",
|
||||
"video/adaptation/overuse_frame_detector.h",
|
||||
"video/call_stats.h",
|
||||
@ -2522,6 +2570,7 @@ webrtc_sources = [
|
||||
"api/stats/rtc_stats_collector_callback.h",
|
||||
"api/transport/rtp/rtp_source.h",
|
||||
"api/video/recordable_encoded_frame.h",
|
||||
"api/video/render_resolution.h",
|
||||
"api/video_codecs/bitstream_parser.h",
|
||||
"api/video_codecs/vp8_frame_buffer_controller.h",
|
||||
"media/base/delayable.h",
|
||||
@ -2545,6 +2594,7 @@ webrtc_sources = [
|
||||
"modules/audio_coding/codecs/isac/main/source/pitch_filter.h",
|
||||
"modules/audio_device/audio_device_config.h",
|
||||
"modules/audio_device/include/audio_device_default.h",
|
||||
"modules/audio_device/include/audio_device_factory.h",
|
||||
"modules/audio_processing/aec3/delay_estimate.h",
|
||||
"modules/audio_processing/aec3/vector_math.h",
|
||||
"modules/audio_processing/agc/gain_control.h",
|
||||
@ -2576,6 +2626,9 @@ webrtc_sources = [
|
||||
"call/audio_sender.h",
|
||||
"call/rtp_transport_controller_send_interface.h",
|
||||
"call/rtp_video_sender_interface.h",
|
||||
"call/rtp_transport_config.h",
|
||||
"call/rtp_transport_controller_send_factory.h",
|
||||
"call/rtp_transport_controller_send_factory_interface.h",
|
||||
"modules/audio_coding/audio_network_adaptor/util/threshold_curve.h",
|
||||
"modules/audio_coding/codecs/ilbc/cb_mem_energy.h",
|
||||
"modules/audio_coding/codecs/ilbc/do_plc.h",
|
||||
@ -2628,6 +2681,8 @@ webrtc_sources = [
|
||||
"p2p/base/dtls_transport_factory.h",
|
||||
"p2p/base/udp_port.h",
|
||||
"pc/peer_connection_internal.h",
|
||||
"pc/peer_connection_factory_proxy.h",
|
||||
"pc/peer_connection_proxy.h",
|
||||
"pc/used_ids.h",
|
||||
"rtc_base/numerics/divide_round.h",
|
||||
"rtc_base/system/thread_registry.h",
|
||||
@ -2854,6 +2909,7 @@ webrtc_sources = [
|
||||
"api/video_codecs/vp9_profile.cc",
|
||||
"api/video_codecs/h264_profile_level_id.h",
|
||||
"api/video_codecs/h264_profile_level_id.cc",
|
||||
"api/video_track_source_proxy_factory.h",
|
||||
"modules/remote_bitrate_estimator/packet_arrival_map.h",
|
||||
"modules/remote_bitrate_estimator/packet_arrival_map.cc",
|
||||
"modules/audio_processing/agc/clipping_predictor.h",
|
||||
|
Loading…
x
Reference in New Issue
Block a user