mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
f33d7acadb
@ -1392,13 +1392,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let context = strongSelf.context
|
||||||
|
let filterPeersAreMuted: Signal<Bool, NoError> = strongSelf.context.engine.peers.currentChatListFilters()
|
||||||
|
|> take(1)
|
||||||
|
|> mapToSignal { filters -> Signal<Bool, NoError> in
|
||||||
|
guard let filter = filters.first(where: { $0.id == id }) else {
|
||||||
|
return .single(false)
|
||||||
|
}
|
||||||
|
guard case let .filter(_, _, _, data) = filter else {
|
||||||
|
return .single(false)
|
||||||
|
}
|
||||||
|
return context.engine.data.get(
|
||||||
|
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.NotificationSettings.init(id:)))
|
||||||
|
)
|
||||||
|
|> map { list -> Bool in
|
||||||
|
for item in list {
|
||||||
|
switch item.muteState {
|
||||||
|
case .default, .unmuted:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _ = combineLatest(
|
let _ = combineLatest(
|
||||||
queue: Queue.mainQueue(),
|
queue: Queue.mainQueue(),
|
||||||
strongSelf.context.engine.peers.currentChatListFilters(),
|
strongSelf.context.engine.peers.currentChatListFilters(),
|
||||||
strongSelf.context.engine.data.get(
|
strongSelf.context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||||
)
|
),
|
||||||
).start(next: { [weak self] filters, premiumLimits in
|
filterPeersAreMuted
|
||||||
|
).start(next: { [weak self] filters, premiumLimits, filterPeersAreMuted in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1546,6 +1574,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
|
|
||||||
for filter in filters {
|
for filter in filters {
|
||||||
if filter.id == filterId, case let .filter(_, title, _, data) = filter {
|
if filter.id == filterId, case let .filter(_, title, _, data) = filter {
|
||||||
|
if data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty && !data.includePeers.peers.isEmpty {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: filterPeersAreMuted ? "Unmute All" : "Mute All", textColor: .primary, badge: nil, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: filterPeersAreMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { c, f in
|
||||||
|
c.dismiss(completion: {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = strongSelf.context.engine.peers.updateMultiplePeerMuteSettings(peerIds: data.includePeers.peers, muted: !filterPeersAreMuted).start()
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
if !data.includePeers.peers.isEmpty && data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty {
|
if !data.includePeers.peers.isEmpty && data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty {
|
||||||
items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: data.hasSharedLinks ? nil : ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: data.hasSharedLinks ? nil : ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||||
@ -2701,13 +2743,53 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) {
|
private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) {
|
||||||
openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in
|
let presentationData = self.presentationData
|
||||||
|
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||||
|
self?.present(controller, in: .window(.root))
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.8, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
|
||||||
|
let signal: Signal<[ExportedChatFolderLink]?, NoError> = self.context.engine.peers.getExportedChatFolderLinks(id: filterId)
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = (signal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] links in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let previewScreen = ChatFolderLinkPreviewScreen(
|
||||||
|
context: self.context,
|
||||||
|
subject: .linkList(folderId: filterId, initialLinks: links ?? []),
|
||||||
|
contents: ChatFolderLinkContents(
|
||||||
|
localFilterId: filterId, title: title,
|
||||||
|
peers: [],
|
||||||
|
alreadyMemberPeerIds: Set(),
|
||||||
|
memberCounts: [:]
|
||||||
|
),
|
||||||
|
completion: nil
|
||||||
|
)
|
||||||
|
self.push(previewScreen)
|
||||||
|
})
|
||||||
|
|
||||||
|
/*openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in
|
||||||
self?.push(c)
|
self?.push(c)
|
||||||
}, presentController: { [weak self] c in
|
}, presentController: { [weak self] c in
|
||||||
self?.present(c, in: .window(.root))
|
self?.present(c, in: .window(.root))
|
||||||
}, completed: {
|
}, completed: {
|
||||||
}, linkUpdated: { _ in
|
}, linkUpdated: { _ in
|
||||||
})
|
})*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) {
|
public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) {
|
||||||
|
@ -1521,22 +1521,10 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
|||||||
|> deliverOnMainQueue).start(next: { filters in
|
|> deliverOnMainQueue).start(next: { filters in
|
||||||
updated(filters)
|
updated(filters)
|
||||||
|
|
||||||
if let currentPreset, waitForSync {
|
if waitForSync {
|
||||||
let _ = (context.engine.peers.updatedChatListFilters()
|
let _ = (context.engine.peers.chatListFiltersAreSynced()
|
||||||
|> filter { filters -> Bool in
|
|> filter { $0 }
|
||||||
for filter in filters {
|
|
||||||
if filter.id == currentPreset.id {
|
|
||||||
if let data = filter.data {
|
|
||||||
if Set(data.includePeers.peers) == Set(includePeers.peers) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> delay(1.0, queue: .mainQueue())
|
|
||||||
|> deliverOnMainQueue).start(next: { _ in
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
completed()
|
completed()
|
||||||
})
|
})
|
||||||
|
@ -166,7 +166,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
let arguments = arguments as! FolderInviteLinkListControllerArguments
|
let arguments = arguments as! FolderInviteLinkListControllerArguments
|
||||||
switch self {
|
switch self {
|
||||||
case let .header(text):
|
case let .header(text):
|
||||||
return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListNewFolder", sectionId: self.section)
|
return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListCloudFolderLink", sectionId: self.section)
|
||||||
case let .mainLinkHeader(text):
|
case let .mainLinkHeader(text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .mainLink(link, isGenerating):
|
case let .mainLink(link, isGenerating):
|
||||||
@ -517,13 +517,22 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var isGroup = true
|
var isGroup = true
|
||||||
|
let isPrivate = peer.addressName == nil
|
||||||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||||
isGroup = false
|
isGroup = false
|
||||||
}
|
}
|
||||||
if isGroup {
|
if isGroup {
|
||||||
text = "You don't have the admin rights to share invite links to this group chat."
|
if isPrivate {
|
||||||
|
text = "You don't have the admin rights to share invite links to this private group chat."
|
||||||
|
} else {
|
||||||
|
text = "You don't have the admin rights to share invite links to this group chat."
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
text = "You don't have the admin rights to share invite links to this channel."
|
if isPrivate {
|
||||||
|
text = "You don't have the admin rights to share invite links to this private channel."
|
||||||
|
} else {
|
||||||
|
text = "You don't have the admin rights to share invite links to this channel."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismissTooltipsImpl?()
|
dismissTooltipsImpl?()
|
||||||
|
@ -151,7 +151,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
|
|||||||
return (layout, { [weak self] in
|
return (layout, { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if strongSelf.item == nil {
|
if strongSelf.item == nil {
|
||||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .count(1), mode: .direct(cachePathPrefix: nil))
|
||||||
strongSelf.animationNode.visibility = true
|
strongSelf.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
|
@ -197,6 +197,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode {
|
|||||||
badgeIconName: badgeIconName,
|
badgeIconName: badgeIconName,
|
||||||
badgeText: "\(item.count)",
|
badgeText: "\(item.count)",
|
||||||
badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount),
|
badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount),
|
||||||
|
badgeGraphPosition: CGFloat(item.count) / CGFloat(item.premiumCount),
|
||||||
isPremiumDisabled: item.isPremiumDisabled
|
isPremiumDisabled: item.isPremiumDisabled
|
||||||
))
|
))
|
||||||
let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0)
|
let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0)
|
||||||
|
@ -44,6 +44,7 @@ private class PremiumLimitAnimationComponent: Component {
|
|||||||
private let textColor: UIColor
|
private let textColor: UIColor
|
||||||
private let badgeText: String?
|
private let badgeText: String?
|
||||||
private let badgePosition: CGFloat
|
private let badgePosition: CGFloat
|
||||||
|
private let badgeGraphPosition: CGFloat
|
||||||
private let isPremiumDisabled: Bool
|
private let isPremiumDisabled: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -53,6 +54,7 @@ private class PremiumLimitAnimationComponent: Component {
|
|||||||
textColor: UIColor,
|
textColor: UIColor,
|
||||||
badgeText: String?,
|
badgeText: String?,
|
||||||
badgePosition: CGFloat,
|
badgePosition: CGFloat,
|
||||||
|
badgeGraphPosition: CGFloat,
|
||||||
isPremiumDisabled: Bool
|
isPremiumDisabled: Bool
|
||||||
) {
|
) {
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
@ -61,6 +63,7 @@ private class PremiumLimitAnimationComponent: Component {
|
|||||||
self.textColor = textColor
|
self.textColor = textColor
|
||||||
self.badgeText = badgeText
|
self.badgeText = badgeText
|
||||||
self.badgePosition = badgePosition
|
self.badgePosition = badgePosition
|
||||||
|
self.badgeGraphPosition = badgeGraphPosition
|
||||||
self.isPremiumDisabled = isPremiumDisabled
|
self.isPremiumDisabled = isPremiumDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +86,9 @@ private class PremiumLimitAnimationComponent: Component {
|
|||||||
if lhs.badgePosition != rhs.badgePosition {
|
if lhs.badgePosition != rhs.badgePosition {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
|
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -245,7 +251,7 @@ private class PremiumLimitAnimationComponent: Component {
|
|||||||
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight))
|
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight))
|
||||||
self.container.frame = containerFrame
|
self.container.frame = containerFrame
|
||||||
|
|
||||||
let activityPosition: CGFloat = floor(containerFrame.width * component.badgePosition)
|
let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition)
|
||||||
let activeWidth: CGFloat = containerFrame.width - activityPosition
|
let activeWidth: CGFloat = containerFrame.width - activityPosition
|
||||||
|
|
||||||
if !component.isPremiumDisabled {
|
if !component.isPremiumDisabled {
|
||||||
@ -433,6 +439,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
|||||||
let badgeIconName: String?
|
let badgeIconName: String?
|
||||||
let badgeText: String?
|
let badgeText: String?
|
||||||
let badgePosition: CGFloat
|
let badgePosition: CGFloat
|
||||||
|
let badgeGraphPosition: CGFloat
|
||||||
let isPremiumDisabled: Bool
|
let isPremiumDisabled: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -447,6 +454,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
|||||||
badgeIconName: String?,
|
badgeIconName: String?,
|
||||||
badgeText: String?,
|
badgeText: String?,
|
||||||
badgePosition: CGFloat,
|
badgePosition: CGFloat,
|
||||||
|
badgeGraphPosition: CGFloat,
|
||||||
isPremiumDisabled: Bool
|
isPremiumDisabled: Bool
|
||||||
) {
|
) {
|
||||||
self.inactiveColor = inactiveColor
|
self.inactiveColor = inactiveColor
|
||||||
@ -460,6 +468,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
|||||||
self.badgeIconName = badgeIconName
|
self.badgeIconName = badgeIconName
|
||||||
self.badgeText = badgeText
|
self.badgeText = badgeText
|
||||||
self.badgePosition = badgePosition
|
self.badgePosition = badgePosition
|
||||||
|
self.badgeGraphPosition = badgeGraphPosition
|
||||||
self.isPremiumDisabled = isPremiumDisabled
|
self.isPremiumDisabled = isPremiumDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,6 +506,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
|||||||
if lhs.badgePosition != rhs.badgePosition {
|
if lhs.badgePosition != rhs.badgePosition {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
|
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -524,6 +536,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
|||||||
textColor: component.activeTitleColor,
|
textColor: component.activeTitleColor,
|
||||||
badgeText: component.badgeText,
|
badgeText: component.badgeText,
|
||||||
badgePosition: component.badgePosition,
|
badgePosition: component.badgePosition,
|
||||||
|
badgeGraphPosition: component.badgeGraphPosition,
|
||||||
isPremiumDisabled: component.isPremiumDisabled
|
isPremiumDisabled: component.isPremiumDisabled
|
||||||
),
|
),
|
||||||
availableSize: CGSize(width: context.availableSize.width, height: height),
|
availableSize: CGSize(width: context.availableSize.width, height: height),
|
||||||
@ -591,7 +604,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
|||||||
transition: context.transition
|
transition: context.transition
|
||||||
)
|
)
|
||||||
|
|
||||||
let activityPosition = floor(context.availableSize.width * component.badgePosition)
|
let activityPosition = floor(context.availableSize.width * component.badgeGraphPosition)
|
||||||
|
|
||||||
var inactiveValueOpacity: CGFloat = 1.0
|
var inactiveValueOpacity: CGFloat = 1.0
|
||||||
if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 {
|
if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 {
|
||||||
@ -747,6 +760,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
let defaultValue: String
|
let defaultValue: String
|
||||||
let premiumValue: String
|
let premiumValue: String
|
||||||
let badgePosition: CGFloat
|
let badgePosition: CGFloat
|
||||||
|
let badgeGraphPosition: CGFloat
|
||||||
switch subject {
|
switch subject {
|
||||||
case .folders:
|
case .folders:
|
||||||
let limit = state.limits.maxFoldersCount
|
let limit = state.limits.maxFoldersCount
|
||||||
@ -757,6 +771,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
defaultValue = component.count > limit ? "\(limit)" : ""
|
defaultValue = component.count > limit ? "\(limit)" : ""
|
||||||
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
||||||
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
||||||
|
badgeGraphPosition = badgePosition
|
||||||
|
|
||||||
if !state.isPremium && badgePosition > 0.5 {
|
if !state.isPremium && badgePosition > 0.5 {
|
||||||
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
|
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
|
||||||
@ -775,6 +790,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
defaultValue = component.count > limit ? "\(limit)" : ""
|
defaultValue = component.count > limit ? "\(limit)" : ""
|
||||||
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
||||||
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
||||||
|
badgeGraphPosition = badgePosition
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
badgeText = "\(limit)"
|
badgeText = "\(limit)"
|
||||||
@ -794,6 +810,11 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
|
string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
|
||||||
defaultValue = count > limit ? "\(limit)" : ""
|
defaultValue = count > limit ? "\(limit)" : ""
|
||||||
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
|
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
|
||||||
|
if count >= premiumLimit {
|
||||||
|
badgeGraphPosition = max(0.1, CGFloat(limit) / CGFloat(premiumLimit))
|
||||||
|
} else {
|
||||||
|
badgeGraphPosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit))
|
||||||
|
}
|
||||||
badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit))
|
badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit))
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
@ -809,6 +830,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
defaultValue = component.count > limit ? "\(limit)" : ""
|
defaultValue = component.count > limit ? "\(limit)" : ""
|
||||||
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
||||||
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
||||||
|
badgeGraphPosition = badgePosition
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
badgeText = "\(limit)"
|
badgeText = "\(limit)"
|
||||||
@ -823,6 +845,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
defaultValue = component.count > limit ? "\(limit)" : ""
|
defaultValue = component.count > limit ? "\(limit)" : ""
|
||||||
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
|
||||||
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
|
||||||
|
badgeGraphPosition = badgePosition
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
badgeText = "\(limit)"
|
badgeText = "\(limit)"
|
||||||
@ -837,6 +860,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
||||||
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
|
||||||
badgePosition = component.count == 4 ? 1.0 : 0.5
|
badgePosition = component.count == 4 ? 1.0 : 0.5
|
||||||
|
badgeGraphPosition = badgePosition
|
||||||
titleText = strings.Premium_FileTooLarge
|
titleText = strings.Premium_FileTooLarge
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
@ -856,6 +880,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
} else {
|
} else {
|
||||||
badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit))
|
badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit))
|
||||||
}
|
}
|
||||||
|
badgeGraphPosition = badgePosition
|
||||||
buttonAnimationName = "premium_addone"
|
buttonAnimationName = "premium_addone"
|
||||||
|
|
||||||
if isPremiumDisabled {
|
if isPremiumDisabled {
|
||||||
@ -931,6 +956,7 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
badgeIconName: iconName,
|
badgeIconName: iconName,
|
||||||
badgeText: badgeText,
|
badgeText: badgeText,
|
||||||
badgePosition: badgePosition,
|
badgePosition: badgePosition,
|
||||||
|
badgeGraphPosition: badgeGraphPosition,
|
||||||
isPremiumDisabled: isPremiumDisabled
|
isPremiumDisabled: isPremiumDisabled
|
||||||
),
|
),
|
||||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||||
|
@ -170,6 +170,7 @@ private final class LimitComponent: CombinedComponent {
|
|||||||
badgeIconName: "",
|
badgeIconName: "",
|
||||||
badgeText: nil,
|
badgeText: nil,
|
||||||
badgePosition: 0.0,
|
badgePosition: 0.0,
|
||||||
|
badgeGraphPosition: 0.0,
|
||||||
isPremiumDisabled: false
|
isPremiumDisabled: false
|
||||||
),
|
),
|
||||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||||
|
@ -241,7 +241,7 @@ public final class QrCodeScreen: ViewController {
|
|||||||
case .chatFolder:
|
case .chatFolder:
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
title = "Invite by QR Code"
|
title = "Invite by QR Code"
|
||||||
text = "Everyone on Telegram can scan this code to join your folder."
|
text = "Everyone on Telegram can scan this code to add this folder and join the chats included in this invite link."
|
||||||
default:
|
default:
|
||||||
title = ""
|
title = ""
|
||||||
text = ""
|
text = ""
|
||||||
|
@ -107,7 +107,7 @@ extension UserLimitsConfiguration {
|
|||||||
self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength)
|
self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength)
|
||||||
self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText)
|
self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText)
|
||||||
self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1)
|
self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1)
|
||||||
self.maxSharedFolderInviteLinks = getValue("chatlists_invites_limit", orElse: isPremium ? 100 : 3)
|
self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3)
|
||||||
self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2)
|
self.maxSharedFolderJoin = getValue("chatlist_joined_limit", orElse: isPremium ? 100 : 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1319,6 +1319,14 @@ func requestChatListFiltersSync(transaction: Transaction) {
|
|||||||
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeChatListFiltersOperation(content: .sync))
|
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeChatListFiltersOperation(content: .sync))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _internal_chatListFiltersAreSynced(postbox: Postbox) -> Signal<Bool, NoError> {
|
||||||
|
return postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizeChatListFilters, limit: 1)
|
||||||
|
|> map { view -> Bool in
|
||||||
|
return view.entries.isEmpty
|
||||||
|
}
|
||||||
|
|> distinctUntilChanged
|
||||||
|
}
|
||||||
|
|
||||||
func managedChatListFilters(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Void, NoError> {
|
func managedChatListFilters(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Void, NoError> {
|
||||||
return Signal { _ in
|
return Signal { _ in
|
||||||
let updateFeaturedDisposable = _internal_updateChatListFeaturedFilters(postbox: postbox, network: network).start()
|
let updateFeaturedDisposable = _internal_updateChatListFeaturedFilters(postbox: postbox, network: network).start()
|
||||||
|
@ -245,6 +245,15 @@ public extension TelegramEngine {
|
|||||||
public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal<Void, NoError> {
|
public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal<Void, NoError> {
|
||||||
return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval)
|
return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateMultiplePeerMuteSettings(peerIds: [EnginePeer.Id], muted: Bool) -> Signal<Never, NoError> {
|
||||||
|
return self.account.postbox.transaction { transaction -> Void in
|
||||||
|
for peerId in peerIds {
|
||||||
|
_internal_updatePeerMuteSetting(account: self.account, transaction: transaction, peerId: peerId, threadId: nil, muteInterval: muted ? Int32.max : nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
|
||||||
public func updatePeerDisplayPreviewsSetting(peerId: PeerId, threadId: Int64?, displayPreviews: PeerNotificationDisplayPreviews) -> Signal<Void, NoError> {
|
public func updatePeerDisplayPreviewsSetting(peerId: PeerId, threadId: Int64?, displayPreviews: PeerNotificationDisplayPreviews) -> Signal<Void, NoError> {
|
||||||
return _internal_updatePeerDisplayPreviewsSetting(account: self.account, peerId: peerId, threadId: threadId, displayPreviews: displayPreviews)
|
return _internal_updatePeerDisplayPreviewsSetting(account: self.account, peerId: peerId, threadId: threadId, displayPreviews: displayPreviews)
|
||||||
@ -512,6 +521,10 @@ public extension TelegramEngine {
|
|||||||
public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> {
|
public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> {
|
||||||
return _internal_updatedChatListFilters(postbox: self.account.postbox, hiddenIds: self.account.viewTracker.hiddenChatListFilterIds)
|
return _internal_updatedChatListFilters(postbox: self.account.postbox, hiddenIds: self.account.viewTracker.hiddenChatListFilterIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func chatListFiltersAreSynced() -> Signal<Bool, NoError> {
|
||||||
|
return _internal_chatListFiltersAreSynced(postbox: self.account.postbox)
|
||||||
|
}
|
||||||
|
|
||||||
public func updatedChatListFiltersInfo() -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> {
|
public func updatedChatListFiltersInfo() -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> {
|
||||||
return _internal_updatedChatListFiltersInfo(postbox: self.account.postbox)
|
return _internal_updatedChatListFiltersInfo(postbox: self.account.postbox)
|
||||||
|
@ -31,6 +31,8 @@ swift_library(
|
|||||||
"//submodules/Markdown",
|
"//submodules/Markdown",
|
||||||
"//submodules/UndoUI",
|
"//submodules/UndoUI",
|
||||||
"//submodules/PremiumUI",
|
"//submodules/PremiumUI",
|
||||||
|
"//submodules/QrCodeUI",
|
||||||
|
"//submodules/InviteLinksUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -0,0 +1,196 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
final class ActionListItemComponent: Component {
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let iconName: String?
|
||||||
|
let title: String
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
sideInset: CGFloat,
|
||||||
|
iconName: String?,
|
||||||
|
title: String,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.iconName = iconName
|
||||||
|
self.title = title
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.iconName != rhs.iconName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let iconView: UIImageView
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
|
||||||
|
private var highlightBackgroundFrame: CGRect?
|
||||||
|
private var highlightBackgroundLayer: SimpleLayer?
|
||||||
|
|
||||||
|
private var component: ActionListItemComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.containerButton = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.iconView = UIImageView()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
self.addSubview(self.containerButton)
|
||||||
|
|
||||||
|
self.containerButton.addSubview(self.iconView)
|
||||||
|
|
||||||
|
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
||||||
|
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHighlighted {
|
||||||
|
self.superview?.bringSubviewToFront(self)
|
||||||
|
|
||||||
|
let highlightBackgroundLayer: SimpleLayer
|
||||||
|
if let current = self.highlightBackgroundLayer {
|
||||||
|
highlightBackgroundLayer = current
|
||||||
|
} else {
|
||||||
|
highlightBackgroundLayer = SimpleLayer()
|
||||||
|
self.highlightBackgroundLayer = highlightBackgroundLayer
|
||||||
|
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
|
||||||
|
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
|
||||||
|
}
|
||||||
|
highlightBackgroundLayer.frame = highlightBackgroundFrame
|
||||||
|
highlightBackgroundLayer.opacity = 1.0
|
||||||
|
} else {
|
||||||
|
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
|
||||||
|
self.highlightBackgroundLayer = nil
|
||||||
|
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
|
||||||
|
highlightBackgroundLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
if self.component?.iconName != component.iconName {
|
||||||
|
if let iconName = component.iconName {
|
||||||
|
self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate)
|
||||||
|
} else {
|
||||||
|
self.iconView.image = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if themeUpdated {
|
||||||
|
self.iconView.tintColor = component.theme.list.itemAccentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let contextInset: CGFloat = 0.0
|
||||||
|
|
||||||
|
let height: CGFloat = 44.0
|
||||||
|
let verticalInset: CGFloat = 1.0
|
||||||
|
let leftInset: CGFloat = 62.0 + component.sideInset
|
||||||
|
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||||
|
|
||||||
|
let previousTitleFrame = self.title.view?.frame
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let centralContentHeight: CGFloat = titleSize.height
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(titleView)
|
||||||
|
}
|
||||||
|
titleView.frame = titleFrame
|
||||||
|
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||||
|
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let iconImage = self.iconView.image {
|
||||||
|
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
self.separatorLayer.isHidden = !component.hasNext
|
||||||
|
|
||||||
|
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||||
|
|
||||||
|
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||||
|
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,9 @@ import Markdown
|
|||||||
import UndoUI
|
import UndoUI
|
||||||
import PremiumUI
|
import PremiumUI
|
||||||
import ButtonComponent
|
import ButtonComponent
|
||||||
|
import ContextUI
|
||||||
|
import QrCodeUI
|
||||||
|
import InviteLinksUI
|
||||||
|
|
||||||
private final class ChatFolderLinkPreviewScreenComponent: Component {
|
private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -102,6 +105,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
|
|
||||||
private var selectedItems = Set<EnginePeer.Id>()
|
private var selectedItems = Set<EnginePeer.Id>()
|
||||||
|
|
||||||
|
private var linkListItems: [ExportedChatFolderLink] = []
|
||||||
|
|
||||||
private let bottomOverscrollLimit: CGFloat
|
private let bottomOverscrollLimit: CGFloat
|
||||||
|
|
||||||
private var ignoreScrolling: Bool = false
|
private var ignoreScrolling: Bool = false
|
||||||
@ -324,6 +329,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.component == nil, case let .linkList(_, initialLinks) = component.subject {
|
||||||
|
self.linkListItems = initialLinks
|
||||||
|
}
|
||||||
|
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
@ -364,7 +373,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
|
|
||||||
let titleString: String
|
let titleString: String
|
||||||
var allChatsAdded = false
|
var allChatsAdded = false
|
||||||
if let linkContents = component.linkContents {
|
if case .linkList = component.subject {
|
||||||
|
//TODO:localize
|
||||||
|
titleString = "Share Folder"
|
||||||
|
} else if let linkContents = component.linkContents {
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
if case .remove = component.subject {
|
if case .remove = component.subject {
|
||||||
titleString = "Remove Folder"
|
titleString = "Remove Folder"
|
||||||
@ -406,7 +418,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
contentHeight += 14.0
|
contentHeight += 14.0
|
||||||
|
|
||||||
var topBadge: String?
|
var topBadge: String?
|
||||||
if case .remove = component.subject {
|
if case .linkList = component.subject {
|
||||||
|
} else if case .remove = component.subject {
|
||||||
} else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil {
|
} else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil {
|
||||||
topBadge = "+\(linkContents.peers.count)"
|
topBadge = "+\(linkContents.peers.count)"
|
||||||
}
|
}
|
||||||
@ -435,7 +448,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
contentHeight += 20.0
|
contentHeight += 20.0
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
if let linkContents = component.linkContents {
|
if case .linkList = component.subject {
|
||||||
|
text = "Create more links to set up different access\nlevels for different people."
|
||||||
|
} else if let linkContents = component.linkContents {
|
||||||
if case .remove = component.subject {
|
if case .remove = component.subject {
|
||||||
text = "Do you also want to quit the chats included in this folder?"
|
text = "Do you also want to quit the chats included in this folder?"
|
||||||
} else if allChatsAdded {
|
} else if allChatsAdded {
|
||||||
@ -493,90 +508,257 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
|
|
||||||
var itemsHeight: CGFloat = 0.0
|
var itemsHeight: CGFloat = 0.0
|
||||||
var validIds: [AnyHashable] = []
|
var validIds: [AnyHashable] = []
|
||||||
if let linkContents = component.linkContents {
|
if case let .linkList(folderId, _) = component.subject {
|
||||||
|
do {
|
||||||
|
let id = AnyHashable("action")
|
||||||
|
validIds.append(id)
|
||||||
|
|
||||||
|
let item: ComponentView<Empty>
|
||||||
|
var itemTransition = transition
|
||||||
|
if let current = self.items[id] {
|
||||||
|
item = current
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
item = ComponentView()
|
||||||
|
self.items[id] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemSize = item.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(ActionListItemComponent(
|
||||||
|
theme: environment.theme,
|
||||||
|
sideInset: 0.0,
|
||||||
|
iconName: "Contact List/LinkActionIcon",
|
||||||
|
title: "Create a New Link",
|
||||||
|
hasNext: !self.linkListItems.isEmpty,
|
||||||
|
action: { [weak self] in
|
||||||
|
self?.openCreateLink()
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
||||||
|
|
||||||
|
if let itemView = item.view {
|
||||||
|
if itemView.superview == nil {
|
||||||
|
self.itemContainerView.addSubview(itemView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsHeight += itemSize.height
|
||||||
|
singleItemHeight = itemSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0 ..< self.linkListItems.count {
|
||||||
|
let link = self.linkListItems[i]
|
||||||
|
|
||||||
|
let id = AnyHashable(link.link)
|
||||||
|
validIds.append(id)
|
||||||
|
|
||||||
|
let item: ComponentView<Empty>
|
||||||
|
var itemTransition = transition
|
||||||
|
if let current = self.items[id] {
|
||||||
|
item = current
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
item = ComponentView()
|
||||||
|
self.items[id] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtitle: String
|
||||||
|
if link.peerIds.count == 1 {
|
||||||
|
subtitle = "includes 1 chat"
|
||||||
|
} else {
|
||||||
|
subtitle = "includes \(link.peerIds.count) chats"
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemComponent = LinkListItemComponent(
|
||||||
|
theme: environment.theme,
|
||||||
|
sideInset: 0.0,
|
||||||
|
title: link.title.isEmpty ? link.link : link.title,
|
||||||
|
link: link,
|
||||||
|
label: subtitle,
|
||||||
|
selectionState: .none,
|
||||||
|
hasNext: i != self.linkListItems.count - 1,
|
||||||
|
action: { [weak self] link in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openLink(link: link)
|
||||||
|
},
|
||||||
|
contextAction: { [weak self] link, sourceView, gesture in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
var itemList: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
itemList.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: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
UIPasteboard.general.string = link.link
|
||||||
|
|
||||||
|
if let self, let component = self.component, let controller = self.environment?.controller() {
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
if let self, let component = self.component, let controller = self.environment?.controller() {
|
||||||
|
controller.present(QrCodeScreen(context: component.context, updatedPresentationData: nil, subject: .chatFolder(slug: link.slug)), in: .window(.root))
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
if let self, let component = self.component {
|
||||||
|
self.linkListItems.removeAll(where: { $0.link == link.link })
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
|
||||||
|
let context = component.context
|
||||||
|
let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true)
|
||||||
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
let _ = (context.engine.peers.deleteChatFolderLink(filterId: folderId, link: link)
|
||||||
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
let items = ContextController.Items(content: .list(itemList))
|
||||||
|
|
||||||
|
let controller = ContextController(
|
||||||
|
account: component.context.account,
|
||||||
|
presentationData: presentationData,
|
||||||
|
source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)),
|
||||||
|
items: .single(items),
|
||||||
|
recognizer: nil,
|
||||||
|
gesture: gesture
|
||||||
|
)
|
||||||
|
|
||||||
|
environment.controller()?.forEachController({ controller in
|
||||||
|
if let controller = controller as? UndoOverlayController {
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
environment.controller()?.presentInGlobalOverlay(controller)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let itemSize = item.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(itemComponent),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
||||||
|
|
||||||
|
if let itemView = item.view {
|
||||||
|
if itemView.superview == nil {
|
||||||
|
self.itemContainerView.addSubview(itemView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsHeight += itemSize.height
|
||||||
|
singleItemHeight = itemSize.height
|
||||||
|
}
|
||||||
|
} else if let linkContents = component.linkContents {
|
||||||
for i in 0 ..< linkContents.peers.count {
|
for i in 0 ..< linkContents.peers.count {
|
||||||
let peer = linkContents.peers[i]
|
let peer = linkContents.peers[i]
|
||||||
|
|
||||||
for _ in 0 ..< 1 {
|
let id = AnyHashable(peer.id)
|
||||||
//let id: AnyHashable = AnyHashable("\(peer.id)_\(j)")
|
validIds.append(id)
|
||||||
let id = AnyHashable(peer.id)
|
|
||||||
validIds.append(id)
|
let item: ComponentView<Empty>
|
||||||
|
var itemTransition = transition
|
||||||
let item: ComponentView<Empty>
|
if let current = self.items[id] {
|
||||||
var itemTransition = transition
|
item = current
|
||||||
if let current = self.items[id] {
|
} else {
|
||||||
item = current
|
itemTransition = .immediate
|
||||||
} else {
|
item = ComponentView()
|
||||||
itemTransition = .immediate
|
self.items[id] = item
|
||||||
item = ComponentView()
|
|
||||||
self.items[id] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
var subtitle: String?
|
|
||||||
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
|
||||||
subtitle = "You are already a member"
|
|
||||||
} else if let memberCount = linkContents.memberCounts[peer.id] {
|
|
||||||
subtitle = "\(memberCount) participants"
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemSize = item.update(
|
|
||||||
transition: itemTransition,
|
|
||||||
component: AnyComponent(PeerListItemComponent(
|
|
||||||
context: component.context,
|
|
||||||
theme: environment.theme,
|
|
||||||
strings: environment.strings,
|
|
||||||
sideInset: 0.0,
|
|
||||||
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
|
||||||
peer: peer,
|
|
||||||
subtitle: subtitle,
|
|
||||||
selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)),
|
|
||||||
hasNext: i != linkContents.peers.count - 1,
|
|
||||||
action: { [weak self] peer in
|
|
||||||
guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if case .remove = component.subject {
|
|
||||||
if self.selectedItems.contains(peer.id) {
|
|
||||||
self.selectedItems.remove(peer.id)
|
|
||||||
} else {
|
|
||||||
self.selectedItems.insert(peer.id)
|
|
||||||
}
|
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
||||||
} else if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
let text: String
|
|
||||||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
|
||||||
text = "You are already a member of this channel."
|
|
||||||
} else {
|
|
||||||
text = "You are already a member of this group."
|
|
||||||
}
|
|
||||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current)
|
|
||||||
} else {
|
|
||||||
if self.selectedItems.contains(peer.id) {
|
|
||||||
self.selectedItems.remove(peer.id)
|
|
||||||
} else {
|
|
||||||
self.selectedItems.insert(peer.id)
|
|
||||||
}
|
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
|
||||||
)
|
|
||||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
|
||||||
|
|
||||||
if let itemView = item.view {
|
|
||||||
if itemView.superview == nil {
|
|
||||||
self.itemContainerView.addSubview(itemView)
|
|
||||||
}
|
|
||||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsHeight += itemSize.height
|
|
||||||
singleItemHeight = itemSize.height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subtitle: String?
|
||||||
|
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
||||||
|
subtitle = "You are already a member"
|
||||||
|
} else if let memberCount = linkContents.memberCounts[peer.id] {
|
||||||
|
subtitle = "\(memberCount) participants"
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemSize = item.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(PeerListItemComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: environment.theme,
|
||||||
|
strings: environment.strings,
|
||||||
|
sideInset: 0.0,
|
||||||
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
|
peer: peer,
|
||||||
|
subtitle: subtitle,
|
||||||
|
selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)),
|
||||||
|
hasNext: i != linkContents.peers.count - 1,
|
||||||
|
action: { [weak self] peer in
|
||||||
|
guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .remove = component.subject {
|
||||||
|
if self.selectedItems.contains(peer.id) {
|
||||||
|
self.selectedItems.remove(peer.id)
|
||||||
|
} else {
|
||||||
|
self.selectedItems.insert(peer.id)
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
} else if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let text: String
|
||||||
|
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||||
|
text = "You are already a member of this channel."
|
||||||
|
} else {
|
||||||
|
text = "You are already a member of this group."
|
||||||
|
}
|
||||||
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current)
|
||||||
|
} else {
|
||||||
|
if self.selectedItems.contains(peer.id) {
|
||||||
|
self.selectedItems.remove(peer.id)
|
||||||
|
} else {
|
||||||
|
self.selectedItems.insert(peer.id)
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
||||||
|
|
||||||
|
if let itemView = item.view {
|
||||||
|
if itemView.superview == nil {
|
||||||
|
self.itemContainerView.addSubview(itemView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsHeight += itemSize.height
|
||||||
|
singleItemHeight = itemSize.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,7 +774,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let listHeaderTitle: String
|
let listHeaderTitle: String
|
||||||
if let linkContents = component.linkContents {
|
if case .linkList = component.subject {
|
||||||
|
listHeaderTitle = "INVITE LINKS"
|
||||||
|
} else if let linkContents = component.linkContents {
|
||||||
if case .remove = component.subject {
|
if case .remove = component.subject {
|
||||||
if linkContents.peers.count == 1 {
|
if linkContents.peers.count == 1 {
|
||||||
listHeaderTitle = "1 CHAT TO QUIT"
|
listHeaderTitle = "1 CHAT TO QUIT"
|
||||||
@ -839,21 +1023,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
|
|
||||||
/*self.joinDisposable = (component.context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(self.selectedItems))
|
|
||||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
|
||||||
guard let self, let controller = self.environment?.controller() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
controller.dismiss()
|
|
||||||
})*/
|
|
||||||
} else if allChatsAdded {
|
} else if allChatsAdded {
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
} else if let _ = component.linkContents {
|
} else if let _ = component.linkContents {
|
||||||
if self.joinDisposable == nil, !self.selectedItems.isEmpty {
|
if self.joinDisposable == nil, !self.selectedItems.isEmpty {
|
||||||
let joinSignal: Signal<JoinChatFolderResult?, JoinChatFolderLinkError>
|
let joinSignal: Signal<JoinChatFolderResult?, JoinChatFolderLinkError>
|
||||||
switch component.subject {
|
switch component.subject {
|
||||||
case .remove:
|
case .linkList, .remove:
|
||||||
return
|
return
|
||||||
case let .slug(slug):
|
case let .slug(slug):
|
||||||
joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems))
|
joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems))
|
||||||
@ -961,40 +1137,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||||
)
|
)
|
||||||
/*let actionButtonSize = self.actionButton.update(
|
|
||||||
transition: transition,
|
|
||||||
component: AnyComponent(SolidRoundedButtonComponent(
|
|
||||||
title: actionButtonTitle,
|
|
||||||
badge: (self.selectedItems.isEmpty || allChatsAdded) ? nil : "\(self.selectedItems.count)",
|
|
||||||
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
|
||||||
font: .bold,
|
|
||||||
fontSize: 17.0,
|
|
||||||
height: 50.0,
|
|
||||||
cornerRadius: 11.0,
|
|
||||||
gloss: false,
|
|
||||||
isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil,
|
|
||||||
animationName: nil,
|
|
||||||
iconPosition: .right,
|
|
||||||
iconSpacing: 4.0,
|
|
||||||
isLoading: self.inProgress,
|
|
||||||
action: { [weak self] in
|
|
||||||
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
|
||||||
)*/
|
|
||||||
let bottomPanelHeight = 14.0 + environment.safeInsets.bottom + actionButtonSize.height
|
|
||||||
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
|
||||||
if let actionButtonView = self.actionButton.view {
|
|
||||||
if actionButtonView.superview == nil {
|
|
||||||
self.addSubview(actionButtonView)
|
|
||||||
}
|
|
||||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight)))
|
var bottomPanelHeight: CGFloat = 0.0
|
||||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
|
||||||
|
if case .linkList = component.subject {
|
||||||
|
bottomPanelHeight += 30.0
|
||||||
|
} else {
|
||||||
|
bottomPanelHeight += 14.0 + environment.safeInsets.bottom + actionButtonSize.height
|
||||||
|
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||||
|
if let actionButtonView = self.actionButton.view {
|
||||||
|
if actionButtonView.superview == nil {
|
||||||
|
self.addSubview(actionButtonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight)))
|
||||||
|
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||||
|
}
|
||||||
|
|
||||||
if let controller = environment.controller() {
|
if let controller = environment.controller() {
|
||||||
let subLayout = ContainerViewLayout(
|
let subLayout = ContainerViewLayout(
|
||||||
@ -1015,9 +1175,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
||||||
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
||||||
|
|
||||||
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
|
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
|
||||||
|
|
||||||
//self.scrollContentClippingView.layer.cornerRadius = 10.0
|
|
||||||
|
|
||||||
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight)
|
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight)
|
||||||
|
|
||||||
@ -1026,12 +1184,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
||||||
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 8.0 - (containerInset + 56.0)))
|
let scrollClippingFrame: CGRect
|
||||||
|
if case .linkList = component.subject {
|
||||||
|
scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - (containerInset + 56.0) + 1000.0))
|
||||||
|
} else {
|
||||||
|
scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - bottomPanelHeight - 8.0 - (containerInset + 56.0)))
|
||||||
|
}
|
||||||
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
||||||
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
||||||
|
|
||||||
self.ignoreScrolling = true
|
self.ignoreScrolling = true
|
||||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset)))
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||||
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
||||||
if contentSize != self.scrollView.contentSize {
|
if contentSize != self.scrollView.contentSize {
|
||||||
self.scrollView.contentSize = contentSize
|
self.scrollView.contentSize = contentSize
|
||||||
@ -1044,6 +1207,168 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openLink(link: ExportedChatFolderLink) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case let .linkList(folderId, _) = component.subject else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.context.engine.peers.currentChatListFilters()
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] filters in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let filter = filters.first(where: { $0.id == folderId }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case let .filter(_, title, _, data) = filter else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let peerIds = data.includePeers.peers
|
||||||
|
let _ = (component.context.engine.data.get(
|
||||||
|
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||||
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let peers = peers.compactMap({ peer -> EnginePeer? in
|
||||||
|
guard let peer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if case let .legacyGroup(group) = peer, group.migrationReference != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return peer
|
||||||
|
})
|
||||||
|
|
||||||
|
let navigationController = controller.navigationController
|
||||||
|
controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in
|
||||||
|
(navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root))
|
||||||
|
}))
|
||||||
|
controller.dismiss()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openCreateLink() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case let .linkList(folderId, _) = component.subject else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.context.engine.peers.currentChatListFilters()
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] filters in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let filter = filters.first(where: { $0.id == folderId }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case let .filter(_, title, _, data) = filter else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let peerIds = data.includePeers.peers
|
||||||
|
let _ = (component.context.engine.data.get(
|
||||||
|
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||||
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let peers = peers.compactMap({ peer -> EnginePeer? in
|
||||||
|
guard let peer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if case let .legacyGroup(group) = peer, group.migrationReference != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return peer
|
||||||
|
})
|
||||||
|
if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) {
|
||||||
|
let navigationController = controller.navigationController
|
||||||
|
controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: { _ in }, presentController: { [weak navigationController] c in
|
||||||
|
(navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root))
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
var enabledPeerIds: [EnginePeer.Id] = []
|
||||||
|
for peer in peers {
|
||||||
|
if canShareLinkToPeer(peer: peer) {
|
||||||
|
enabledPeerIds.append(peer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: enabledPeerIds)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] link in
|
||||||
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.linkListItems.insert(link, at: 0)
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
|
||||||
|
let navigationController = controller.navigationController
|
||||||
|
controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { [weak self] updatedLink in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let index = self.linkListItems.firstIndex(where: { $0.link == link.link }) {
|
||||||
|
if let updatedLink {
|
||||||
|
self.linkListItems[index] = updatedLink
|
||||||
|
} else {
|
||||||
|
self.linkListItems.remove(at: index)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let updatedLink {
|
||||||
|
self.linkListItems.insert(updatedLink, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
}, presentController: { [weak navigationController] c in
|
||||||
|
(navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root))
|
||||||
|
}))
|
||||||
|
|
||||||
|
controller.dismiss()
|
||||||
|
}, error: { [weak self] error in
|
||||||
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let text: String
|
||||||
|
switch error {
|
||||||
|
case .generic:
|
||||||
|
text = "An error occurred"
|
||||||
|
case let .sharedFolderLimitExceeded(limit, _):
|
||||||
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, action: {
|
||||||
|
})
|
||||||
|
|
||||||
|
controller.push(limitController)
|
||||||
|
|
||||||
|
return
|
||||||
|
case let .limitExceeded(limit, _):
|
||||||
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: {
|
||||||
|
})
|
||||||
|
controller.push(limitController)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeView() -> View {
|
func makeView() -> View {
|
||||||
@ -1060,6 +1385,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
|
|||||||
case slug(String)
|
case slug(String)
|
||||||
case updates(ChatFolderUpdates)
|
case updates(ChatFolderUpdates)
|
||||||
case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id])
|
case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id])
|
||||||
|
case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink])
|
||||||
}
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
@ -1116,3 +1442,25 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class LinkListContextExtractedContentSource: ContextExtractedContentSource {
|
||||||
|
let keepInPlace: Bool = false
|
||||||
|
let ignoreContentTouches: Bool = false
|
||||||
|
let blurBackground: Bool = true
|
||||||
|
|
||||||
|
//let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
|
||||||
|
|
||||||
|
private let contentView: ContextExtractedContentContainingView
|
||||||
|
|
||||||
|
init(contentView: ContextExtractedContentContainingView) {
|
||||||
|
self.contentView = contentView
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeView() -> ContextControllerTakeViewInfo? {
|
||||||
|
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||||
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,330 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramCore
|
||||||
|
import CheckNode
|
||||||
|
|
||||||
|
func cancelContextGestures(view: UIView) {
|
||||||
|
if let gestureRecognizers = view.gestureRecognizers {
|
||||||
|
for gesture in gestureRecognizers {
|
||||||
|
if let gesture = gesture as? ContextGesture {
|
||||||
|
gesture.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
cancelContextGestures(view: subview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LinkListItemComponent: Component {
|
||||||
|
enum SelectionState: Equatable {
|
||||||
|
case none
|
||||||
|
case editing(isSelected: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let title: String
|
||||||
|
let link: ExportedChatFolderLink
|
||||||
|
let label: String
|
||||||
|
let selectionState: SelectionState
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: (ExportedChatFolderLink) -> Void
|
||||||
|
let contextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
sideInset: CGFloat,
|
||||||
|
title: String,
|
||||||
|
link: ExportedChatFolderLink,
|
||||||
|
label: String,
|
||||||
|
selectionState: SelectionState,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping (ExportedChatFolderLink) -> Void,
|
||||||
|
contextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.title = title
|
||||||
|
self.link = link
|
||||||
|
self.label = label
|
||||||
|
self.selectionState = selectionState
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
self.contextAction = contextAction
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: LinkListItemComponent, rhs: LinkListItemComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.link != rhs.link {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.label != rhs.label {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.selectionState != rhs.selectionState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: ContextControllerSourceView {
|
||||||
|
private let extractedContainerView: ContextExtractedContentContainingView
|
||||||
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let label = ComponentView<Empty>()
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
private let iconView: UIImageView
|
||||||
|
private let iconBackgroundView: UIImageView
|
||||||
|
|
||||||
|
private var checkLayer: CheckLayer?
|
||||||
|
|
||||||
|
private var isExtractedToContextMenu: Bool = false
|
||||||
|
|
||||||
|
private var highlightBackgroundFrame: CGRect?
|
||||||
|
private var highlightBackgroundLayer: SimpleLayer?
|
||||||
|
|
||||||
|
private var component: LinkListItemComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||||
|
self.containerButton = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.iconView = UIImageView()
|
||||||
|
self.iconBackgroundView = UIImageView()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.extractedContainerView)
|
||||||
|
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||||
|
|
||||||
|
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||||
|
|
||||||
|
self.containerButton.addSubview(self.iconBackgroundView)
|
||||||
|
self.containerButton.addSubview(self.iconView)
|
||||||
|
|
||||||
|
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.containerButton.clipsToBounds = value
|
||||||
|
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
|
||||||
|
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||||
|
}
|
||||||
|
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isExtractedToContextMenu = value
|
||||||
|
|
||||||
|
let mappedTransition: Transition
|
||||||
|
if value {
|
||||||
|
mappedTransition = Transition(transition)
|
||||||
|
} else {
|
||||||
|
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: mappedTransition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
||||||
|
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHighlighted, case .none = component.selectionState {
|
||||||
|
self.superview?.bringSubviewToFront(self)
|
||||||
|
|
||||||
|
let highlightBackgroundLayer: SimpleLayer
|
||||||
|
if let current = self.highlightBackgroundLayer {
|
||||||
|
highlightBackgroundLayer = current
|
||||||
|
} else {
|
||||||
|
highlightBackgroundLayer = SimpleLayer()
|
||||||
|
self.highlightBackgroundLayer = highlightBackgroundLayer
|
||||||
|
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
|
||||||
|
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
|
||||||
|
}
|
||||||
|
highlightBackgroundLayer.frame = highlightBackgroundFrame
|
||||||
|
highlightBackgroundLayer.opacity = 1.0
|
||||||
|
} else {
|
||||||
|
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
|
||||||
|
self.highlightBackgroundLayer = nil
|
||||||
|
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
|
||||||
|
highlightBackgroundLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.activated = { [weak self] gesture, _ in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
gesture.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.contextAction(component.link, self.extractedContainerView, gesture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(component.link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||||
|
|
||||||
|
let height: CGFloat = 60.0
|
||||||
|
let verticalInset: CGFloat = 1.0
|
||||||
|
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||||
|
var iconLeftInset: CGFloat = component.sideInset
|
||||||
|
|
||||||
|
if case let .editing(isSelected) = component.selectionState {
|
||||||
|
leftInset += 48.0
|
||||||
|
iconLeftInset += 48.0
|
||||||
|
|
||||||
|
let checkSize: CGFloat = 22.0
|
||||||
|
|
||||||
|
let checkLayer: CheckLayer
|
||||||
|
if let current = self.checkLayer {
|
||||||
|
checkLayer = current
|
||||||
|
if themeUpdated {
|
||||||
|
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
}
|
||||||
|
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||||
|
} else {
|
||||||
|
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||||
|
self.checkLayer = checkLayer
|
||||||
|
self.containerButton.layer.addSublayer(checkLayer)
|
||||||
|
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||||
|
checkLayer.setSelected(isSelected, animated: false)
|
||||||
|
checkLayer.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||||
|
} else {
|
||||||
|
if let checkLayer = self.checkLayer {
|
||||||
|
self.checkLayer = nil
|
||||||
|
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||||
|
checkLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.iconBackgroundView.image = generateFilledCircleImage(diameter: 40.0, color: component.theme.list.itemCheckColors.fillColor)
|
||||||
|
self.iconView.image = UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate)
|
||||||
|
self.iconView.tintColor = component.theme.list.itemCheckColors.foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if let iconImage = self.iconView.image {
|
||||||
|
transition.setFrame(view: self.iconBackgroundView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - 40.0) * 0.5), y: floor((height - 40.0) * 0.5)), size: CGSize(width: 40.0, height: 40.0)))
|
||||||
|
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconImage.size.width) * 0.5), y: floor((height - iconImage.size.height) * 0.5)), size: iconImage.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelSize = self.label.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.label, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat = 1.0
|
||||||
|
|
||||||
|
let contentHeight: CGFloat = titleSize.height + titleSpacing + labelSize.height
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - contentHeight) / 2.0)), size: titleSize)
|
||||||
|
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelSize)
|
||||||
|
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
titleView.layer.anchorPoint = CGPoint()
|
||||||
|
self.containerButton.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||||
|
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||||
|
}
|
||||||
|
if let labelView = self.label.view {
|
||||||
|
if labelView.superview == nil {
|
||||||
|
labelView.isUserInteractionEnabled = false
|
||||||
|
labelView.layer.anchorPoint = CGPoint()
|
||||||
|
self.containerButton.addSubview(labelView)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: labelView, position: labelFrame.origin)
|
||||||
|
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
self.separatorLayer.isHidden = !component.hasNext
|
||||||
|
|
||||||
|
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||||
|
|
||||||
|
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
|
||||||
|
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
|
||||||
|
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
|
||||||
|
self.extractedContainerView.contentRect = resultBounds
|
||||||
|
|
||||||
|
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||||
|
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,19 +15,6 @@ import TelegramStringFormatting
|
|||||||
|
|
||||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||||
|
|
||||||
private func cancelContextGestures(view: UIView) {
|
|
||||||
if let gestureRecognizers = view.gestureRecognizers {
|
|
||||||
for gesture in gestureRecognizers {
|
|
||||||
if let gesture = gesture as? ContextGesture {
|
|
||||||
gesture.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for subview in view.subviews {
|
|
||||||
cancelContextGestures(view: subview)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class PeerListItemComponent: Component {
|
final class PeerListItemComponent: Component {
|
||||||
enum SelectionState: Equatable {
|
enum SelectionState: Equatable {
|
||||||
case none
|
case none
|
||||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user