Folder updates

This commit is contained in:
Ali 2023-03-31 19:28:06 +04:00
parent 33ffaffc8d
commit dfdde96d63
38 changed files with 738 additions and 209 deletions

View File

@ -9118,3 +9118,5 @@ Sorry for the inconvenience.";
"Premium.MaxSharedFolderLinksFinalText" = "Sorry, you can only create **%1$@** invite links"; "Premium.MaxSharedFolderLinksFinalText" = "Sorry, you can only create **%1$@** invite links";
"Premium.GiftedTitle.Someone" = "Someone"; "Premium.GiftedTitle.Someone" = "Someone";
"Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)";

View File

@ -20,4 +20,6 @@ public protocol ChatListController: ViewController {
func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
func playSignUpCompletedAnimation() func playSignUpCompletedAnimation()
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
} }

View File

@ -1546,7 +1546,7 @@ 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.includePeers.peers.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: ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: 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)
}, action: { c, f in }, action: { c, f in
@ -2709,6 +2709,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}) })
} }
public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) {
let _ = (self.chatListDisplayNode.mainContainerNode.availableFiltersSignal
|> filter { filters in
return filters.contains(where: { item in
if case let .filter(filter) = item, filter.id == folderId {
return true
} else {
return false
}
})
}
|> take(1)
|> map { _ -> Bool in
return true
}
|> timeout(1.0, queue: .mainQueue(), alternate: .single(false))
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
if self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id != folderId {
self.chatListDisplayNode.mainContainerNode.switchToFilter(id: .filter(folderId), completion: {
completion()
})
} else {
completion()
}
})
}
private func askForFilterRemoval(id: Int32) { private func askForFilterRemoval(id: Int32) {
let apply: () -> Void = { [weak self] in let apply: () -> Void = { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -2753,18 +2784,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case let .filter(_, title, _, data) = filter, data.isShared { if case let .filter(_, title, _, data) = filter, data.isShared {
let _ = (combineLatest( let _ = (combineLatest(
self.context.engine.data.get( self.context.engine.data.get(
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))),
EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:)))
), ),
self.context.engine.peers.getExportedChatFolderLinks(id: id), self.context.engine.peers.getExportedChatFolderLinks(id: id),
self.context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id) self.context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id)
) )
|> deliverOnMainQueue).start(next: { [weak self] peers, links, defaultSelectedPeerIds in |> deliverOnMainQueue).start(next: { [weak self] peerData, links, defaultSelectedPeerIds in
guard let self else { guard let self else {
return return
} }
let presentationData = self.presentationData let presentationData = self.presentationData
let peers = peerData.0
var memberCounts: [EnginePeer.Id: Int] = [:]
for (id, count) in peerData.1 {
if let count {
memberCounts[id] = count
}
}
var hasLinks = false var hasLinks = false
if let links, !links.isEmpty { if let links, !links.isEmpty {
hasLinks = true hasLinks = true
@ -2788,7 +2829,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false return false
} }
}, },
alreadyMemberPeerIds: Set() alreadyMemberPeerIds: Set(),
memberCounts: memberCounts
), ),
completion: { [weak self] in completion: { [weak self] in
guard let self else { guard let self else {

View File

@ -513,7 +513,16 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:] private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:]
private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)? private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)?
private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] {
didSet {
self.availableFiltersPromise.set(self.availableFilters)
}
}
private let availableFiltersPromise = ValuePromise<[ChatListContainerNodeFilter]>([.all], ignoreRepeated: true)
var availableFiltersSignal: Signal<[ChatListContainerNodeFilter], NoError> {
return self.availableFiltersPromise.get()
}
private var filtersLimit: Int32? = nil private var filtersLimit: Int32? = nil
private var selectedId: ChatListFilterTabEntryId private var selectedId: ChatListFilterTabEntryId

View File

@ -31,10 +31,12 @@ private final class ChatListFilterPresetControllerArguments {
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
let clearFocus: () -> Void
let focusOnName: () -> Void let focusOnName: () -> Void
let expandSection: (FilterSection) -> Void let expandSection: (FilterSection) -> Void
let createLink: () -> Void let createLink: () -> Void
let openLink: (ExportedChatFolderLink) -> Void let openLink: (ExportedChatFolderLink) -> Void
let removeLink: (ExportedChatFolderLink) -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -46,10 +48,12 @@ private final class ChatListFilterPresetControllerArguments {
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void, setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void, deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void, deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
clearFocus: @escaping () -> Void,
focusOnName: @escaping () -> Void, focusOnName: @escaping () -> Void,
expandSection: @escaping (FilterSection) -> Void, expandSection: @escaping (FilterSection) -> Void,
createLink: @escaping () -> Void, createLink: @escaping () -> Void,
openLink: @escaping (ExportedChatFolderLink) -> Void openLink: @escaping (ExportedChatFolderLink) -> Void,
removeLink: @escaping (ExportedChatFolderLink) -> Void
) { ) {
self.context = context self.context = context
self.updateState = updateState self.updateState = updateState
@ -60,10 +64,12 @@ private final class ChatListFilterPresetControllerArguments {
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
self.deleteIncludeCategory = deleteIncludeCategory self.deleteIncludeCategory = deleteIncludeCategory
self.deleteExcludeCategory = deleteExcludeCategory self.deleteExcludeCategory = deleteExcludeCategory
self.clearFocus = clearFocus
self.focusOnName = focusOnName self.focusOnName = focusOnName
self.expandSection = expandSection self.expandSection = expandSection
self.createLink = createLink self.createLink = createLink
self.openLink = openLink self.openLink = openLink
self.removeLink = removeLink
} }
} }
@ -316,10 +322,10 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case excludePeerInfo(String) case excludePeerInfo(String)
case includeExpand(String) case includeExpand(String)
case excludeExpand(String) case excludeExpand(String)
case inviteLinkHeader case inviteLinkHeader(hasLinks: Bool)
case inviteLinkCreate(hasLinks: Bool) case inviteLinkCreate(hasLinks: Bool)
case inviteLink(Int, ExportedChatFolderLink) case inviteLink(Int, ExportedChatFolderLink)
case inviteLinkInfo case inviteLinkInfo(text: String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
@ -434,14 +440,16 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case let .nameHeader(title): case let .nameHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .name(placeholder, value): case let .name(placeholder, value):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .done, clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in arguments.updateState { current in
var state = current var state = current
state.name = value state.name = value
state.changedName = true state.changedName = true
return state return state
} }
}, action: {}, cleared: { }, action: {
arguments.clearFocus()
}, cleared: {
arguments.focusOnName() arguments.focusOnName()
}) })
case .includePeersHeader(let text), .excludePeersHeader(let text): case .includePeersHeader(let text), .excludePeersHeader(let text):
@ -516,23 +524,23 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandSection(.exclude) arguments.expandSection(.exclude)
}) })
case .inviteLinkHeader: case let .inviteLinkHeader(hasLinks):
//TODO:localize //TODO:localize
return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: "SHARE FOLDER", badge: hasLinks ? nil : "NEW", sectionId: self.section)
case let .inviteLinkCreate(hasLinks): case let .inviteLinkCreate(hasLinks):
//TODO:localize //TODO:localize
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? "Create a New Link" : "Share Folder", sectionId: self.section, editing: false, action: { let _ = hasLinks
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "Create an Invite Link", sectionId: self.section, editing: false, action: {
arguments.createLink() arguments.createLink()
}) })
case let .inviteLink(_, link): case let .inviteLink(_, link):
return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks) { invite in return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite) arguments.openLink(invite)
} contextAction: { invite, node, gesture in }, removeAction: { invite in
//arguments.linkContextAction(invite, canEdit, node, gesture) arguments.removeLink(invite)
} }, contextAction: nil)
case .inviteLinkInfo: case let .inviteLinkInfo(text):
//TODO:localize return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
return ItemListTextItem(presentationData: presentationData, text: .markdown("Share access to some of this folder's groups and channels with others."), sectionId: self.section)
} }
} }
} }
@ -654,13 +662,12 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
} }
if !isNewFilter {
entries.append(.inviteLinkHeader)
var hasLinks = false var hasLinks = false
if let inviteLinks, !inviteLinks.isEmpty { if let inviteLinks, !inviteLinks.isEmpty {
hasLinks = true hasLinks = true
} }
entries.append(.inviteLinkHeader(hasLinks: hasLinks))
entries.append(.inviteLinkCreate(hasLinks: hasLinks)) entries.append(.inviteLinkCreate(hasLinks: hasLinks))
if let inviteLinks { if let inviteLinks {
@ -671,8 +678,8 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
} }
} }
entries.append(.inviteLinkInfo) //TODO:localize
} entries.append(.inviteLinkInfo(text: hasLinks ? "Create more links to set up different access levels for different people." : "Share access to some of this folder's groups and channels with others."))
return entries return entries
} }
@ -1072,6 +1079,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var focusOnNameImpl: (() -> Void)? var focusOnNameImpl: (() -> Void)?
var clearFocusImpl: (() -> Void)?
var applyImpl: ((@escaping () -> Void) -> Void)? var applyImpl: ((@escaping () -> Void) -> Void)?
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil) let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
@ -1270,6 +1278,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
return state return state
} }
}, },
clearFocus: {
clearFocusImpl?()
},
focusOnName: { focusOnName: {
focusOnNameImpl?() focusOnNameImpl?()
}, },
@ -1281,14 +1292,23 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} }
}, },
createLink: { createLink: {
if currentPreset == nil {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text = "Please finish creating this folder to share it."
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
} else {
applyImpl?({ applyImpl?({
let state = stateValue.with({ $0 }) let state = stateValue.with({ $0 })
if let currentPreset, let data = currentPreset.data { if let currentPreset, let data = currentPreset.data {
//TODO:localize //TODO:localize
var unavailableText: String? var unavailableText: String?
if !data.categories.isEmpty || data.excludeArchived || data.excludeRead || data.excludeMuted || !data.excludePeers.isEmpty { if !data.categories.isEmpty {
unavailableText = "You can't share a link to this folder." unavailableText = "You cant share folders with include chat types."
} else if data.excludeArchived || data.excludeRead || data.excludeMuted {
unavailableText = "You can only share folders without chat types and excluded chats."
} else if !data.excludePeers.isEmpty {
unavailableText = "You cant share folders with excluded chats"
} }
if let unavailableText { if let unavailableText {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -1297,11 +1317,15 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
return return
} }
var previousLink: ExportedChatFolderLink?
openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in
pushControllerImpl?(c) pushControllerImpl?(c)
}, presentController: { c in }, presentController: { c in
presentControllerImpl?(c, nil) presentControllerImpl?(c, nil)
}, linkUpdated: { updatedLink in }, linkUpdated: { updatedLink in
let previousLinkValue = previousLink
previousLink = updatedLink
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
guard var links else { guard var links else {
return return
@ -1313,12 +1337,17 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} else { } else {
links.insert(updatedLink, at: 0) links.insert(updatedLink, at: 0)
} }
} else if let previousLinkValue {
if let index = links.firstIndex(where: { $0.link == previousLinkValue.link }) {
links.remove(at: index)
}
}
sharedLinks.set(.single(links)) sharedLinks.set(.single(links))
}
}) })
}) })
} }
}) })
}
}, openLink: { link in }, openLink: { link in
if let currentPreset, let _ = currentPreset.data { if let currentPreset, let _ = currentPreset.data {
applyImpl?({ applyImpl?({
@ -1331,13 +1360,13 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} }
if let updatedLink { if let updatedLink {
if let index = links.firstIndex(where: { $0 == link }) { if let index = links.firstIndex(where: { $0.link == link.link }) {
links.remove(at: index) links.remove(at: index)
} }
links.insert(updatedLink, at: 0) links.insert(updatedLink, at: 0)
sharedLinks.set(.single(links)) sharedLinks.set(.single(links))
} else { } else {
if let index = links.firstIndex(where: { $0 == link }) { if let index = links.firstIndex(where: { $0.link == link.link }) {
links.remove(at: index) links.remove(at: index)
sharedLinks.set(.single(links)) sharedLinks.set(.single(links))
} }
@ -1347,6 +1376,22 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
})) }))
}) })
} }
},
removeLink: { link in
if let currentPreset {
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
guard var links else {
return
}
if let index = links.firstIndex(where: { $0.link == link.link }) {
links.remove(at: index)
}
sharedLinks.set(.single(links))
actionsDisposable.add(context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: link).start())
})
}
} }
) )
@ -1462,6 +1507,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} }
} }
} }
clearFocusImpl = { [weak controller] in
guard let controller = controller else {
return
}
controller.view.endEditing(true)
}
controller.attemptNavigation = { _ in controller.attemptNavigation = { _ in
return attemptNavigationImpl?() ?? true return attemptNavigationImpl?() ?? true
} }
@ -1493,7 +1544,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
return false return false
} }
} else { } else {
if state.isComplete { if currentPreset != nil, state.isComplete {
displaySaveAlert() displaySaveAlert()
return false return false
} }

View File

@ -393,14 +393,23 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
if case let .filter(_, title, _, data) = filter, data.isShared { if case let .filter(_, title, _, data) = filter, data.isShared {
let _ = (combineLatest( let _ = (combineLatest(
context.engine.data.get( context.engine.data.get(
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))),
EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:)))
), ),
context.engine.peers.getExportedChatFolderLinks(id: id), context.engine.peers.getExportedChatFolderLinks(id: id),
context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id) context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id)
) )
|> deliverOnMainQueue).start(next: { peers, links, defaultSelectedPeerIds in |> deliverOnMainQueue).start(next: { peerData, links, defaultSelectedPeerIds in
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let peers = peerData.0
var memberCounts: [EnginePeer.Id: Int] = [:]
for (id, count) in peerData.1 {
if let count {
memberCounts[id] = count
}
}
var hasLinks = false var hasLinks = false
if let links, !links.isEmpty { if let links, !links.isEmpty {
hasLinks = true hasLinks = true
@ -420,7 +429,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
return false return false
} }
}, },
alreadyMemberPeerIds: Set() alreadyMemberPeerIds: Set(),
memberCounts: memberCounts
) )
) )
pushControllerImpl?(previewScreen) pushControllerImpl?(previewScreen)

View File

@ -215,7 +215,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
} else { } else {
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
} }
updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Share"), color: item.presentationData.theme.list.disclosureArrowColor) updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor)
} }
let peerRevealOptions: [ItemListRevealOption] let peerRevealOptions: [ItemListRevealOption]

View File

@ -69,7 +69,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool) case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool)
case peersHeader(String) case peersHeader(String)
case peer(index: Int, peer: EnginePeer, isSelected: Bool, isEnabled: Bool) case peer(index: Int, peer: EnginePeer, isSelected: Bool, disabledReasonText: String?)
case peersInfo(String) case peersInfo(String)
var section: ItemListSectionId { var section: ItemListSectionId {
@ -149,8 +149,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .peer(index, peer, isSelected, isEnabled): case let .peer(index, peer, isSelected, disabledReasonText):
if case .peer(index, peer, isSelected, isEnabled) = rhs { if case .peer(index, peer, isSelected, disabledReasonText) = rhs {
return true return true
} else { } else {
return false return false
@ -195,7 +195,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peersInfo(text): case let .peersInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .peer(_, peer, isSelected, isEnabled): case let .peer(_, peer, isSelected, disabledReasonText):
//TODO:localize //TODO:localize
return ItemListPeerItem( return ItemListPeerItem(
presentationData: presentationData, presentationData: presentationData,
@ -204,16 +204,16 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
context: arguments.context, context: arguments.context,
peer: peer, peer: peer,
presence: nil, presence: nil,
text: .text(isEnabled ? "you can invite others here" : "you can't invite others here", .secondary), text: .text(disabledReasonText ?? "you can invite others here", .secondary),
label: .none, label: .none,
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled), switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: disabledReasonText == nil),
enabled: true, enabled: true,
selectable: true, selectable: true,
highlightable: false, highlightable: false,
sectionId: self.section, sectionId: self.section,
action: { action: {
arguments.peerAction(peer, isEnabled) arguments.peerAction(peer, disabledReasonText == nil)
}, },
setPeerIdWithRevealedOptions: { _, _ in setPeerIdWithRevealedOptions: { _, _ in
}, },
@ -274,8 +274,19 @@ private func folderInviteLinkListControllerEntries(
} }
for peer in sortedPeers { for peer in sortedPeers {
let isEnabled = canShareLinkToPeer(peer: peer) var disabledReasonText: String?
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled)) if !canShareLinkToPeer(peer: peer) {
if case let .user(user) = peer {
if user.botInfo != nil {
disabledReasonText = "you can't share chats with bots"
} else {
disabledReasonText = "you can't share private chats"
}
} else {
disabledReasonText = "you can't invite other here"
}
}
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), disabledReasonText: disabledReasonText))
} }
if let infoString { if let infoString {
@ -391,7 +402,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
let state = stateValue.with({ $0 }) let state = stateValue.with({ $0 })
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Link title", value: state.title ?? "", apply: { value in let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Name This Link", titleFont: .bold, value: state.title ?? "", apply: { value in
if let value { if let value {
updateState { state in updateState { state in
var state = state var state = state
@ -458,8 +469,11 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
state.selectedPeerIds.remove(peer.id) state.selectedPeerIds.remove(peer.id)
} else { } else {
state.selectedPeerIds.insert(peer.id) state.selectedPeerIds.insert(peer.id)
if let currentInvitation, !currentInvitation.peerIds.contains(peer.id) {
added = true added = true
} }
}
return state return state
} }
@ -469,7 +483,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
dismissTooltipsImpl?() dismissTooltipsImpl?()
//TODO:localize //TODO:localize
displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.")) displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.", timeout: 8))
} }
} else { } else {
//TODO:localize //TODO:localize
@ -549,7 +563,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
dismissTooltipsImpl?() dismissTooltipsImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize //TODO:localize
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred.", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}, completed: { }, completed: {
linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false)) linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false))
dismissImpl?() dismissImpl?()
@ -587,6 +601,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
} }
}) })
let previousState = Atomic<FolderInviteLinkListControllerState?>(value: nil)
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(), let signal = combineLatest(queue: .mainQueue(),
presentationData, presentationData,
@ -595,7 +611,13 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
) )
|> map { presentationData, state, allPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, state, allPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
let crossfade = false let crossfade = false
let animateChanges = false
var animateChanges = false
let previousStateValue = previousState.swap(state)
if let previousStateValue, previousStateValue.selectedPeerIds != state.selectedPeerIds {
animateChanges = true
}
//TODO:localize //TODO:localize
let title: ItemListControllerTitle let title: ItemListControllerTitle
@ -610,7 +632,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
if state.isSaving { if state.isSaving {
doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else { } else {
doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty, action: {
applyChangesImpl?() applyChangesImpl?()
}) })
} }

View File

@ -66,7 +66,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case requestHeader(PresentationTheme, String, String, Bool) case requestHeader(PresentationTheme, String, String, Bool)
case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool)
case importerHeader(PresentationTheme, String, String, Bool) case importerHeader(PresentationTheme, String, String, Bool)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool)
var stableId: InviteLinkViewEntryId { var stableId: InviteLinkViewEntryId {
switch self { switch self {
@ -82,7 +82,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
return .request(peer.id) return .request(peer.id)
case .importerHeader: case .importerHeader:
return .importerHeader return .importerHeader
case let .importer(_, _, _, peer, _, _): case let .importer(_, _, _, peer, _, _, _):
return .importer(peer.id) return .importer(peer.id)
} }
} }
@ -125,8 +125,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading): case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsLoading == rhsLoading { if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading {
return true return true
} else { } else {
return false return false
@ -180,11 +180,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case .importer: case .importer:
return true return true
} }
case let .importer(lhsIndex, _, _, _, _, _): case let .importer(lhsIndex, _, _, _, _, _, _):
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader:
return false return false
case let .importer(rhsIndex, _, _, _, _, _): case let .importer(rhsIndex, _, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} }
} }
@ -224,7 +224,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
additionalText = .none additionalText = .none
} }
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText) return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText)
case let .importer(_, _, dateTimeFormat, peer, date, loading), let .request(_, _, dateTimeFormat, peer, date, loading): case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading):
let dateString: String
if joinedViaFolderLink {
//TODO:localize
dateString = "joined via a folder invite link"
} else {
dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
}
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: {
interaction.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil)
case let .request(_, _, dateTimeFormat, peer, date, loading):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: {
interaction.openPeer(peer.id) interaction.openPeer(peer.id)
@ -753,14 +764,14 @@ public final class InviteLinkViewController: ViewController {
loading = true loading = true
let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: []) let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [])
for i in 0 ..< count { for i in 0 ..< count {
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true))
} }
} else { } else {
count = min(4, Int32(state.importers.count)) count = min(4, Int32(state.importers.count))
loading = false loading = false
for importer in state.importers { for importer in state.importers {
if let peer = importer.peer.peer { if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, false)) entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false))
} }
index += 1 index += 1
} }

View File

@ -38,6 +38,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let tapAction: ((ExportedChatFolderLink) -> Void)? let tapAction: ((ExportedChatFolderLink) -> Void)?
let removeAction: ((ExportedChatFolderLink) -> Void)?
let contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)? let contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
@ -48,6 +49,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
sectionId: ItemListSectionId, sectionId: ItemListSectionId,
style: ItemListStyle, style: ItemListStyle,
tapAction: ((ExportedChatFolderLink) -> Void)?, tapAction: ((ExportedChatFolderLink) -> Void)?,
removeAction: ((ExportedChatFolderLink) -> Void)?,
contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?, contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?,
tag: ItemListItemTag? = nil tag: ItemListItemTag? = nil
) { ) {
@ -57,6 +59,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.tapAction = tapAction self.tapAction = tapAction
self.removeAction = removeAction
self.contextAction = contextAction self.contextAction = contextAction
self.tag = tag self.tag = tag
} }
@ -125,7 +128,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
} }
} }
public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListItemNode { public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -506,6 +509,15 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte
strongSelf.placeholderNode = nil strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode() shimmerNode.removeFromSupernode()
} }
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.removeAction != nil {
//TODO:localize
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: "Delete", icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
} else {
strongSelf.setRevealOptions((left: [], right: []))
}
} }
}) })
} }
@ -565,6 +577,21 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte
shimmerNode.updateAbsoluteRect(rect, within: containerSize) shimmerNode.updateAbsoluteRect(rect, within: containerSize)
} }
} }
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
transition.updateSublayerTransformOffset(layer: self.containerNode.layer, offset: CGPoint(x: offset, y: 0.0))
}
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.layoutParams?.0, let invite = item.invite {
item.removeAction?(invite)
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
} }
private struct ContentParticle { private struct ContentParticle {

View File

@ -541,7 +541,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
if data.count > settings.maxSize { if data.count > settings.maxSize {
presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)) presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil))
souceUrl.stopAccessingSecurityScopedResource() souceUrl.stopAccessingSecurityScopedResource()
TempBox.shared.dispose(tempFile) TempBox.shared.dispose(tempFile)
@ -594,7 +594,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co
if duration > Double(settings.maxDuration) { if duration > Double(settings.maxDuration) {
souceUrl.stopAccessingSecurityScopedResource() souceUrl.stopAccessingSecurityScopedResource()
presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil))
} else { } else {
Logger.shared.log("NotificationSoundSelection", "Uploading sound") Logger.shared.log("NotificationSoundSelection", "Uploading sound")

View File

@ -150,6 +150,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
private final class PromptAlertContentNode: AlertContentNode { private final class PromptAlertContentNode: AlertContentNode {
private let strings: PresentationStrings private let strings: PresentationStrings
private let text: String private let text: String
private let titleFont: PromptControllerTitleFont
private let textNode: ASTextNode private let textNode: ASTextNode
let inputFieldNode: PromptInputFieldNode let inputFieldNode: PromptInputFieldNode
@ -174,9 +175,10 @@ private final class PromptAlertContentNode: AlertContentNode {
return self.isUserInteractionEnabled return self.isUserInteractionEnabled
} }
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String?) { init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, value: String?) {
self.strings = strings self.strings = strings
self.text = text self.text = text
self.titleFont = titleFont
self.textNode = ASTextNode() self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 2
@ -244,7 +246,14 @@ private final class PromptAlertContentNode: AlertContentNode {
} }
override func updateTheme(_ theme: AlertControllerTheme) { override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) let titleFontValue: UIFont
switch self.titleFont {
case .regular:
titleFontValue = Font.regular(13.0)
case .bold:
titleFontValue = Font.semibold(17.0)
}
self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes { for actionNode in self.actionNodes {
@ -379,7 +388,12 @@ private final class PromptAlertContentNode: AlertContentNode {
} }
} }
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, value: String?, apply: @escaping (String?) -> Void) -> AlertController { public enum PromptControllerTitleFont {
case regular
case bold
}
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, value: String?, apply: @escaping (String?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)? var dismissImpl: ((Bool) -> Void)?
@ -393,7 +407,7 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
applyImpl?() applyImpl?()
})] })]
let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, value: value) let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, value: value)
contentNode.complete = { contentNode.complete = {
applyImpl?() applyImpl?()
} }

View File

@ -238,6 +238,10 @@ public final class QrCodeScreen: ViewController {
case let .invite(_, isGroup): case let .invite(_, isGroup):
title = self.presentationData.strings.InviteLink_QRCode_Title title = self.presentationData.strings.InviteLink_QRCode_Title
text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel
case .chatFolder:
//TODO:localize
title = "Invite by QR Code"
text = "Everyone on Telegram can scan this code to join your folder."
default: default:
title = "" title = ""
text = "" text = ""

View File

@ -470,7 +470,7 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat
let presentGlobalController = context.sharedContext.presentGlobalController let presentGlobalController = context.sharedContext.presentGlobalController
let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: false).start(completed: { let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: false).start(completed: {
Queue.mainQueue().after(0.1) { Queue.mainQueue().after(0.1) {
presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
} }
}) })
}) })

View File

@ -920,7 +920,7 @@ public func privacyAndSecurityController(
hapticFeedback.impact() hapticFeedback.impact()
var alreadyPresented = false var alreadyPresented = false
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip), elevatedLayout: false, animateInAsReplacement: false, action: { action in presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if action == .info { if action == .info {
if !alreadyPresented { if !alreadyPresented {
let controller = PremiumIntroScreen(context: context, source: .settings) let controller = PremiumIntroScreen(context: context, source: .settings)

View File

@ -1487,7 +1487,7 @@ public final class SolidRoundedButtonView: UIView {
badgeNode = current badgeNode = current
} else { } else {
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor) badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
badgeNode.alpha = self.titleNode.alpha badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0
self.badgeNode = badgeNode self.badgeNode = badgeNode
self.addSubnode(badgeNode) self.addSubnode(badgeNode)
} }

View File

@ -118,7 +118,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1091179342] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteRevoke($0) } dict[1091179342] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteRevoke($0) }
dict[-484690728] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantInvite($0) } dict[-484690728] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantInvite($0) }
dict[405815507] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoin($0) } dict[405815507] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoin($0) }
dict[1557846647] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) } dict[-23084712] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) }
dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) } dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) }
dict[-124291086] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantLeave($0) } dict[-124291086] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantLeave($0) }
dict[-115071790] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantMute($0) } dict[-115071790] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantMute($0) }

View File

@ -700,7 +700,7 @@ public extension Api {
case channelAdminLogEventActionExportedInviteRevoke(invite: Api.ExportedChatInvite) case channelAdminLogEventActionExportedInviteRevoke(invite: Api.ExportedChatInvite)
case channelAdminLogEventActionParticipantInvite(participant: Api.ChannelParticipant) case channelAdminLogEventActionParticipantInvite(participant: Api.ChannelParticipant)
case channelAdminLogEventActionParticipantJoin case channelAdminLogEventActionParticipantJoin
case channelAdminLogEventActionParticipantJoinByInvite(invite: Api.ExportedChatInvite) case channelAdminLogEventActionParticipantJoinByInvite(flags: Int32, invite: Api.ExportedChatInvite)
case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64) case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64)
case channelAdminLogEventActionParticipantLeave case channelAdminLogEventActionParticipantLeave
case channelAdminLogEventActionParticipantMute(participant: Api.GroupCallParticipant) case channelAdminLogEventActionParticipantMute(participant: Api.GroupCallParticipant)
@ -878,10 +878,11 @@ public extension Api {
} }
break break
case .channelAdminLogEventActionParticipantJoinByInvite(let invite): case .channelAdminLogEventActionParticipantJoinByInvite(let flags, let invite):
if boxed { if boxed {
buffer.appendInt32(1557846647) buffer.appendInt32(-23084712)
} }
serializeInt32(flags, buffer: buffer, boxed: false)
invite.serialize(buffer, true) invite.serialize(buffer, true)
break break
case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy):
@ -1059,8 +1060,8 @@ public extension Api {
return ("channelAdminLogEventActionParticipantInvite", [("participant", participant as Any)]) return ("channelAdminLogEventActionParticipantInvite", [("participant", participant as Any)])
case .channelAdminLogEventActionParticipantJoin: case .channelAdminLogEventActionParticipantJoin:
return ("channelAdminLogEventActionParticipantJoin", []) return ("channelAdminLogEventActionParticipantJoin", [])
case .channelAdminLogEventActionParticipantJoinByInvite(let invite): case .channelAdminLogEventActionParticipantJoinByInvite(let flags, let invite):
return ("channelAdminLogEventActionParticipantJoinByInvite", [("invite", invite as Any)]) return ("channelAdminLogEventActionParticipantJoinByInvite", [("flags", flags as Any), ("invite", invite as Any)])
case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy):
return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", invite as Any), ("approvedBy", approvedBy as Any)]) return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", invite as Any), ("approvedBy", approvedBy as Any)])
case .channelAdminLogEventActionParticipantLeave: case .channelAdminLogEventActionParticipantLeave:
@ -1431,13 +1432,16 @@ public extension Api {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoin return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoin
} }
public static func parse_channelAdminLogEventActionParticipantJoinByInvite(_ reader: BufferReader) -> ChannelAdminLogEventAction? { public static func parse_channelAdminLogEventActionParticipantJoinByInvite(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
var _1: Api.ExportedChatInvite? var _1: Int32?
_1 = reader.readInt32()
var _2: Api.ExportedChatInvite?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite _2 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite
} }
let _c1 = _1 != nil let _c1 = _1 != nil
if _c1 { let _c2 = _2 != nil
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByInvite(invite: _1!) if _c1 && _c2 {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByInvite(flags: _1!, invite: _2!)
} }
else { else {
return nil return nil

View File

@ -1574,7 +1574,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
}).start() }).start()
} }
strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false }) strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess, timeout: nil), action: { _ in return false })
} }
}) })
self?.controller?.present(controller, in: .window(.root)) self?.controller?.present(controller, in: .window(.root))
@ -1595,7 +1595,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
if let strongSelf = self, let (firstName, lastName) = firstAndLastName { if let strongSelf = self, let (firstName, lastName) = firstAndLastName {
let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start() let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start()
strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess), action: { _ in return false }) strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess, timeout: nil), action: { _ in return false })
} }
}) })
self?.controller?.present(controller, in: .window(.root)) self?.controller?.present(controller, in: .window(.root))

View File

@ -73,7 +73,7 @@ public enum AdminLogEventAction {
case deleteExportedInvitation(ExportedInvitation) case deleteExportedInvitation(ExportedInvitation)
case revokeExportedInvitation(ExportedInvitation) case revokeExportedInvitation(ExportedInvitation)
case editExportedInvitation(previous: ExportedInvitation, updated: ExportedInvitation) case editExportedInvitation(previous: ExportedInvitation, updated: ExportedInvitation)
case participantJoinedViaInvite(ExportedInvitation) case participantJoinedViaInvite(invitation: ExportedInvitation, joinedViaFolderLink: Bool)
case changeHistoryTTL(previousValue: Int32?, updatedValue: Int32?) case changeHistoryTTL(previousValue: Int32?, updatedValue: Int32?)
case changeTheme(previous: String?, updated: String?) case changeTheme(previous: String?, updated: String?)
case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId) case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId)
@ -268,8 +268,8 @@ func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, m
action = .revokeExportedInvitation(ExportedInvitation(apiExportedInvite: invite)) action = .revokeExportedInvitation(ExportedInvitation(apiExportedInvite: invite))
case let .channelAdminLogEventActionExportedInviteEdit(prevInvite, newInvite): case let .channelAdminLogEventActionExportedInviteEdit(prevInvite, newInvite):
action = .editExportedInvitation(previous: ExportedInvitation(apiExportedInvite: prevInvite), updated: ExportedInvitation(apiExportedInvite: newInvite)) action = .editExportedInvitation(previous: ExportedInvitation(apiExportedInvite: prevInvite), updated: ExportedInvitation(apiExportedInvite: newInvite))
case let .channelAdminLogEventActionParticipantJoinByInvite(invite): case let .channelAdminLogEventActionParticipantJoinByInvite(flags, invite):
action = .participantJoinedViaInvite(ExportedInvitation(apiExportedInvite: invite)) action = .participantJoinedViaInvite(invitation: ExportedInvitation(apiExportedInvite: invite), joinedViaFolderLink: (flags & (1 << 0)) != 0)
case let .channelAdminLogEventActionParticipantVolume(participant): case let .channelAdminLogEventActionParticipantVolume(participant):
let parsedParticipant = GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate(participant) let parsedParticipant = GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate(participant)
action = .groupCallUpdateParticipantVolume(peerId: parsedParticipant.peerId, volume: parsedParticipant.volume ?? 10000) action = .groupCallUpdateParticipantVolume(peerId: parsedParticipant.peerId, volume: parsedParticipant.volume ?? 10000)

View File

@ -389,6 +389,21 @@ extension ChatListFilter {
case .allChats: case .allChats:
return nil return nil
case let .filter(id, title, emoticon, data): case let .filter(id, title, emoticon, data):
if data.isShared {
var flags: Int32 = 0
if emoticon != nil {
flags |= 1 << 25
}
return .dialogFilterCommunity(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {
return nil
}
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
} else {
var flags: Int32 = 0 var flags: Int32 = 0
if data.excludeMuted { if data.excludeMuted {
flags |= 1 << 11 flags |= 1 << 11
@ -416,6 +431,7 @@ extension ChatListFilter {
} }
} }
} }
}
public enum RequestUpdateChatListFilterError { public enum RequestUpdateChatListFilterError {
case generic case generic
@ -873,14 +889,21 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
struct ChatListFiltersState: Codable, Equatable { struct ChatListFiltersState: Codable, Equatable {
struct ChatListFilterUpdates: Codable, Equatable { struct ChatListFilterUpdates: Codable, Equatable {
struct MemberCount: Codable, Equatable {
var id: PeerId
var count: Int32
}
var folderId: Int32 var folderId: Int32
var timestamp: Int32 var timestamp: Int32
var peerIds: [PeerId] var peerIds: [PeerId]
var memberCounts: [MemberCount]
init(folderId: Int32, timestamp: Int32, peerIds: [PeerId]) { init(folderId: Int32, timestamp: Int32, peerIds: [PeerId], memberCounts: [MemberCount]) {
self.folderId = folderId self.folderId = folderId
self.timestamp = timestamp self.timestamp = timestamp
self.peerIds = peerIds self.peerIds = peerIds
self.memberCounts = memberCounts
} }
} }

View File

@ -7,7 +7,7 @@ public func canShareLinkToPeer(peer: EnginePeer) -> Bool {
var isEnabled = false var isEnabled = false
switch peer { switch peer {
case let .channel(channel): case let .channel(channel):
if channel.adminRights != nil && channel.hasPermission(.inviteMembers) { if channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
isEnabled = true isEnabled = true
} else if channel.username != nil { } else if channel.username != nil {
isEnabled = true isEnabled = true
@ -237,17 +237,20 @@ public final class ChatFolderLinkContents {
public let title: String? public let title: String?
public let peers: [EnginePeer] public let peers: [EnginePeer]
public let alreadyMemberPeerIds: Set<EnginePeer.Id> public let alreadyMemberPeerIds: Set<EnginePeer.Id>
public let memberCounts: [EnginePeer.Id: Int]
public init( public init(
localFilterId: Int32?, localFilterId: Int32?,
title: String?, title: String?,
peers: [EnginePeer], peers: [EnginePeer],
alreadyMemberPeerIds: Set<EnginePeer.Id> alreadyMemberPeerIds: Set<EnginePeer.Id>,
memberCounts: [EnginePeer.Id: Int]
) { ) {
self.localFilterId = localFilterId self.localFilterId = localFilterId
self.title = title self.title = title
self.peers = peers self.peers = peers
self.alreadyMemberPeerIds = alreadyMemberPeerIds self.alreadyMemberPeerIds = alreadyMemberPeerIds
self.memberCounts = memberCounts
} }
} }
@ -264,6 +267,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
var allPeers: [Peer] = [] var allPeers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:] var peerPresences: [PeerId: Api.User] = [:]
var memberCounts: [PeerId: Int] = [:]
for user in users { for user in users {
let telegramUser = TelegramUser(user: user) let telegramUser = TelegramUser(user: user)
@ -274,6 +278,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
if let peer = parseTelegramGroupOrChannel(chat: chat) { if let peer = parseTelegramGroupOrChannel(chat: chat) {
allPeers.append(peer) allPeers.append(peer)
} }
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat {
if let participantsCount = participantsCount {
memberCounts[chat.peerId] = Int(participantsCount)
}
}
} }
updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in
@ -294,12 +303,13 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
} }
alreadyMemberPeerIds.removeAll() alreadyMemberPeerIds.removeAll()
return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds) return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
case let .communityInviteAlready(filterId, missingPeers, alreadyPeers, chats, users): case let .communityInviteAlready(filterId, missingPeers, alreadyPeers, chats, users):
let _ = alreadyPeers let _ = alreadyPeers
var allPeers: [Peer] = [] var allPeers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:] var peerPresences: [PeerId: Api.User] = [:]
var memberCounts: [PeerId: Int] = [:]
for user in users { for user in users {
let telegramUser = TelegramUser(user: user) let telegramUser = TelegramUser(user: user)
@ -310,6 +320,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
if let peer = parseTelegramGroupOrChannel(chat: chat) { if let peer = parseTelegramGroupOrChannel(chat: chat) {
allPeers.append(peer) allPeers.append(peer)
} }
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat {
if let participantsCount = participantsCount {
memberCounts[chat.peerId] = Int(participantsCount)
}
}
} }
updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in
@ -356,7 +371,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
} }
} }
return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds) return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
} }
} }
|> castError(CheckChatFolderLinkError.self) |> castError(CheckChatFolderLinkError.self)
@ -370,12 +385,31 @@ public enum JoinChatFolderLinkError {
case tooManyChannels(limit: Int32, premiumLimit: Int32) case tooManyChannels(limit: Int32, premiumLimit: Int32)
} }
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> { public final class JoinChatFolderResult {
return account.postbox.transaction { transaction -> [Api.InputPeer] in public let folderId: Int32
return peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer) public let title: String
public let newChatCount: Int
public init(folderId: Int32, title: String, newChatCount: Int) {
self.folderId = folderId
self.title = title
self.newChatCount = newChatCount
}
}
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> {
return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in
var newChatCount = 0
for peerId in peerIds {
if transaction.getPeerChatListIndex(peerId) != nil {
newChatCount += 1
}
}
return (peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer), newChatCount)
} }
|> castError(JoinChatFolderLinkError.self) |> castError(JoinChatFolderLinkError.self)
|> mapToSignal { inputPeers -> Signal<Never, JoinChatFolderLinkError> in |> mapToSignal { inputPeers, newChatCount -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> in
return account.network.request(Api.functions.communities.joinCommunityInvite(slug: slug, peers: inputPeers)) return account.network.request(Api.functions.communities.joinCommunityInvite(slug: slug, peers: inputPeers))
|> `catch` { error -> Signal<Api.Updates, JoinChatFolderLinkError> in |> `catch` { error -> Signal<Api.Updates, JoinChatFolderLinkError> in
if error.errorDescription == "USER_CHANNELS_TOO_MUCH" { if error.errorDescription == "USER_CHANNELS_TOO_MUCH" {
@ -427,10 +461,36 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
return .fail(.generic) return .fail(.generic)
} }
} }
|> mapToSignal { result -> Signal<Never, JoinChatFolderLinkError> in |> mapToSignal { result -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> in
account.stateManager.addUpdates(result) account.stateManager.addUpdates(result)
return .complete() var folderResult: JoinChatFolderResult?
for update in result.allUpdates {
if case let .updateDialogFilter(_, id, data) = update {
if let data = data, case let .filter(_, title, _, _) = ChatListFilter(apiFilter: data) {
folderResult = JoinChatFolderResult(folderId: id, title: title, newChatCount: newChatCount)
}
break
}
}
if let folderResult = folderResult {
return _internal_updatedChatListFilters(postbox: account.postbox)
|> castError(JoinChatFolderLinkError.self)
|> filter { filters -> Bool in
if filters.contains(where: { $0.id == folderResult.folderId }) {
return true
} else {
return false
}
}
|> take(1)
|> map { _ -> JoinChatFolderResult in
return folderResult
}
} else {
return .fail(.generic)
}
} }
} }
} }
@ -439,23 +499,26 @@ public final class ChatFolderUpdates: Equatable {
fileprivate let folderId: Int32 fileprivate let folderId: Int32
fileprivate let title: String fileprivate let title: String
fileprivate let missingPeers: [EnginePeer] fileprivate let missingPeers: [EnginePeer]
fileprivate let memberCounts: [EnginePeer.Id: Int]
public var availableChatsToJoin: Int { public var availableChatsToJoin: Int {
return self.missingPeers.count return self.missingPeers.count
} }
public var chatFolderLinkContents: ChatFolderLinkContents { public var chatFolderLinkContents: ChatFolderLinkContents {
return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set()) return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set(), memberCounts: self.memberCounts)
} }
fileprivate init( fileprivate init(
folderId: Int32, folderId: Int32,
title: String, title: String,
missingPeers: [EnginePeer] missingPeers: [EnginePeer],
memberCounts: [EnginePeer.Id: Int]
) { ) {
self.folderId = folderId self.folderId = folderId
self.title = title self.title = title
self.missingPeers = missingPeers self.missingPeers = missingPeers
self.memberCounts = memberCounts
} }
public static func ==(lhs: ChatFolderUpdates, rhs: ChatFolderUpdates) -> Bool { public static func ==(lhs: ChatFolderUpdates, rhs: ChatFolderUpdates) -> Bool {
@ -513,7 +576,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
var state = state var state = state
state.updates.removeAll(where: { $0.folderId == folderId }) state.updates.removeAll(where: { $0.folderId == folderId })
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: [])) state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: [], memberCounts: []))
return state return state
}) })
@ -525,6 +588,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
return account.postbox.transaction { transaction -> Void in return account.postbox.transaction { transaction -> Void in
var peers: [Peer] = [] var peers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:] var peerPresences: [PeerId: Api.User] = [:]
var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = []
for user in users { for user in users {
let telegramUser = TelegramUser(user: user) let telegramUser = TelegramUser(user: user)
@ -535,6 +599,11 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
if let peer = parseTelegramGroupOrChannel(chat: chat) { if let peer = parseTelegramGroupOrChannel(chat: chat) {
peers.append(peer) peers.append(peer)
} }
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat {
if let participantsCount = participantsCount {
memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount))
}
}
} }
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
@ -546,7 +615,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
var state = state var state = state
state.updates.removeAll(where: { $0.folderId == folderId }) state.updates.removeAll(where: { $0.folderId == folderId })
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId))) state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId), memberCounts: memberCounts))
return state return state
}) })
@ -561,6 +630,7 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) ->
struct InternalData: Equatable { struct InternalData: Equatable {
var title: String var title: String
var peerIds: [EnginePeer.Id] var peerIds: [EnginePeer.Id]
var memberCounts: [EnginePeer.Id: Int]
} }
return _internal_updatedChatListFiltersState(postbox: account.postbox) return _internal_updatedChatListFiltersState(postbox: account.postbox)
@ -575,7 +645,11 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) ->
return nil return nil
} }
let filteredPeerIds: [PeerId] = update.peerIds.filter { !data.includePeers.peers.contains($0) } let filteredPeerIds: [PeerId] = update.peerIds.filter { !data.includePeers.peers.contains($0) }
return InternalData(title: title, peerIds: filteredPeerIds) var memberCounts: [PeerId: Int] = [:]
for item in update.memberCounts {
memberCounts[item.id] = Int(item.count)
}
return InternalData(title: title, peerIds: filteredPeerIds, memberCounts: memberCounts)
} }
|> distinctUntilChanged |> distinctUntilChanged
|> mapToSignal { internalData -> Signal<ChatFolderUpdates?, NoError> in |> mapToSignal { internalData -> Signal<ChatFolderUpdates?, NoError> in
@ -592,7 +666,7 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) ->
peers.append(EnginePeer(peer)) peers.append(EnginePeer(peer))
} }
} }
return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers) return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers, memberCounts: internalData.memberCounts)
} }
} }
} }

View File

@ -664,6 +664,7 @@ public struct PeerInvitationImportersState: Equatable {
public var date: Int32 public var date: Int32
public var about: String? public var about: String?
public var approvedBy: PeerId? public var approvedBy: PeerId?
public var joinedViaFolderLink: Bool
} }
public var importers: [Importer] public var importers: [Importer]
public var isLoadingMore: Bool public var isLoadingMore: Bool
@ -708,6 +709,7 @@ final class CachedPeerInvitationImporters: Codable {
let peerIds: [PeerId] let peerIds: [PeerId]
let dates: [PeerId: Int32] let dates: [PeerId: Int32]
let abouts: [PeerId: String] let abouts: [PeerId: String]
let joinedViaFolderLink: [PeerId: Bool]
let count: Int32 let count: Int32
static func key(peerId: PeerId, link: String, requested: Bool) -> ValueBoxKey { static func key(peerId: PeerId, link: String, requested: Bool) -> ValueBoxKey {
@ -727,13 +729,17 @@ final class CachedPeerInvitationImporters: Codable {
$0[$1.peer.peerId] = about $0[$1.peer.peerId] = about
} }
} }
self.joinedViaFolderLink = importers.reduce(into: [PeerId: Bool]()) {
$0[$1.peer.peerId] = $1.joinedViaFolderLink
}
self.count = count self.count = count
} }
init(peerIds: [PeerId], dates: [PeerId: Int32], abouts: [PeerId: String], count: Int32) { init(peerIds: [PeerId], dates: [PeerId: Int32], abouts: [PeerId: String], joinedViaFolderLink: [PeerId: Bool], count: Int32) {
self.peerIds = peerIds self.peerIds = peerIds
self.dates = dates self.dates = dates
self.abouts = abouts self.abouts = abouts
self.joinedViaFolderLink = joinedViaFolderLink
self.count = count self.count = count
} }
@ -752,6 +758,16 @@ final class CachedPeerInvitationImporters: Codable {
} }
self.dates = dates self.dates = dates
var joinedViaFolderLink: [PeerId: Bool] = [:]
let joinedViaFolderLinkArray = try container.decode([Int64].self, forKey: "joinedViaFolderLink")
for index in stride(from: 0, to: joinedViaFolderLinkArray.endIndex, by: 2) {
let userId = datesArray[index]
let value = datesArray[index + 1]
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
joinedViaFolderLink[peerId] = value != 0
}
self.joinedViaFolderLink = joinedViaFolderLink
var abouts: [PeerId: String] = [:] var abouts: [PeerId: String] = [:]
let aboutsArray = try container.decodeIfPresent([DictionaryPair].self, forKey: "abouts") let aboutsArray = try container.decodeIfPresent([DictionaryPair].self, forKey: "abouts")
if let aboutsArray = aboutsArray { if let aboutsArray = aboutsArray {
@ -777,6 +793,13 @@ final class CachedPeerInvitationImporters: Codable {
} }
try container.encode(dates, forKey: "dates") try container.encode(dates, forKey: "dates")
var joinedViaFolderLink: [Int64] = []
for (peerId, value) in self.joinedViaFolderLink {
joinedViaFolderLink.append(peerId.id._internalGetInt64Value())
joinedViaFolderLink.append(Int64(value ? 1 : 0))
}
try container.encode(joinedViaFolderLink, forKey: "joinedViaFolderLink")
var abouts: [DictionaryPair] = [] var abouts: [DictionaryPair] = []
for (peerId, about) in self.abouts { for (peerId, about) in self.abouts {
abouts.append(DictionaryPair(peerId.id._internalGetInt64Value(), value: about)) abouts.append(DictionaryPair(peerId.id._internalGetInt64Value(), value: about))
@ -847,7 +870,7 @@ private final class PeerInvitationImportersContextImpl {
var result: [PeerInvitationImportersState.Importer] = [] var result: [PeerInvitationImportersState.Importer] = []
for peerId in cachedResult.peerIds { for peerId in cachedResult.peerIds {
if let peer = transaction.getPeer(peerId), let date = cachedResult.dates[peerId] { if let peer = transaction.getPeer(peerId), let date = cachedResult.dates[peerId] {
result.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: cachedResult.abouts[peerId])) result.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: cachedResult.abouts[peerId], joinedViaFolderLink: cachedResult.joinedViaFolderLink[peerId] ?? false))
} else { } else {
return nil return nil
} }
@ -958,15 +981,17 @@ private final class PeerInvitationImportersContextImpl {
let date: Int32 let date: Int32
let about: String? let about: String?
let approvedBy: PeerId? let approvedBy: PeerId?
let joinedViaFolderLink: Bool
switch importer { switch importer {
case let .chatInviteImporter(_, userId, dateValue, aboutValue, approvedByValue): case let .chatInviteImporter(flags, userId, dateValue, aboutValue, approvedByValue):
joinedViaFolderLink = (flags & (1 << 3)) != 0
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
date = dateValue date = dateValue
about = aboutValue about = aboutValue
approvedBy = approvedByValue.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } approvedBy = approvedByValue.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }
} }
if let peer = transaction.getPeer(peerId) { if let peer = transaction.getPeer(peerId) {
resultImporters.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: about, approvedBy: approvedBy)) resultImporters.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: about, approvedBy: approvedBy, joinedViaFolderLink: joinedViaFolderLink))
} }
} }
if populateCache && query == nil { if populateCache && query == nil {

View File

@ -1054,7 +1054,7 @@ public extension TelegramEngine {
return _internal_checkChatFolderLink(account: self.account, slug: slug) return _internal_checkChatFolderLink(account: self.account, slug: slug)
} }
public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> { public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> {
return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds) return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds)
} }

View File

@ -55,12 +55,14 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
var containerInset: CGFloat var containerInset: CGFloat
var bottomInset: CGFloat var bottomInset: CGFloat
var topInset: CGFloat var topInset: CGFloat
var contentHeight: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, contentHeight: CGFloat) {
self.containerSize = containerSize self.containerSize = containerSize
self.containerInset = containerInset self.containerInset = containerInset
self.bottomInset = bottomInset self.bottomInset = bottomInset
self.topInset = topInset self.topInset = topInset
self.contentHeight = contentHeight
} }
} }
@ -82,6 +84,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
private let scrollView: ScrollView private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView private let scrollContentView: UIView
private let bottomBackgroundLayer: SimpleLayer
private let bottomSeparatorLayer: SimpleLayer
private let topIcon = ComponentView<Empty>() private let topIcon = ComponentView<Empty>()
@ -134,6 +138,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.itemContainerView.clipsToBounds = true self.itemContainerView.clipsToBounds = true
self.itemContainerView.layer.cornerRadius = 10.0 self.itemContainerView.layer.cornerRadius = 10.0
self.bottomBackgroundLayer = SimpleLayer()
self.bottomSeparatorLayer = SimpleLayer()
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.dimView) self.addSubview(self.dimView)
@ -165,6 +172,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.scrollContentView.addSubview(self.itemContainerView) self.scrollContentView.addSubview(self.itemContainerView)
self.layer.addSublayer(self.bottomBackgroundLayer)
self.layer.addSublayer(self.bottomSeparatorLayer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
} }
@ -231,6 +241,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
topOffset = max(0.0, topOffset) topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
let bottomDistance = itemLayout.contentHeight - self.scrollView.bounds.maxY
let bottomAlphaDistance: CGFloat = 30.0
var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance
bottomAlpha = max(0.0, min(1.0, bottomAlpha))
let bottomOverlayAlpha: CGFloat = bottomAlpha
transition.setAlpha(layer: self.bottomBackgroundLayer, alpha: bottomOverlayAlpha)
transition.setAlpha(layer: self.bottomSeparatorLayer, alpha: bottomOverlayAlpha)
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
@ -244,10 +263,12 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
func animateIn() { func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.backgroundLayer.frame.minY let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomBackgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomSeparatorLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if let actionButtonView = self.actionButton.view { if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
} }
@ -265,6 +286,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
completion() completion()
}) })
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomBackgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomSeparatorLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let actionButtonView = self.actionButton.view { if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
@ -308,6 +331,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
self.bottomBackgroundLayer.backgroundColor = environment.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor
self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
} }
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
@ -485,6 +510,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.items[id] = item 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( let itemSize = item.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent(PeerListItemComponent( component: AnyComponent(PeerListItemComponent(
@ -494,7 +526,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
sideInset: 0.0, sideInset: 0.0,
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer, peer: peer,
subtitle: nil, subtitle: subtitle,
selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)), selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)),
hasNext: i != linkContents.peers.count - 1, hasNext: i != linkContents.peers.count - 1,
action: { [weak self] peer in action: { [weak self] peer in
@ -647,7 +679,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.selectedItems.insert(peerId) self.selectedItems.insert(peerId)
} }
} }
self.state?.updated(transition: .immediate) self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
} }
)), )),
environment: {}, environment: {},
@ -737,21 +769,72 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
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<Never, JoinChatFolderLinkError> let joinSignal: Signal<JoinChatFolderResult?, JoinChatFolderLinkError>
switch component.subject { switch component.subject {
case .remove: case .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))
|> map(Optional.init)
case let .updates(updates): case let .updates(updates):
var result: JoinChatFolderResult?
if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title {
result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: self.selectedItems.count)
}
joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems)) joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems))
|> map { _ -> JoinChatFolderResult? in
}
|> then(Signal<JoinChatFolderResult?, JoinChatFolderLinkError>.single(result))
} }
self.inProgress = true self.inProgress = true
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
self.joinDisposable = (joinSignal self.joinDisposable = (joinSignal
|> deliverOnMainQueue).start(error: { [weak self] error in |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
if let result, let navigationController = controller.navigationController as? NavigationController {
var chatListController: ChatListController?
for viewController in navigationController.viewControllers {
if let rootController = viewController as? TabBarController {
for c in rootController.controllers {
if let c = c as? ChatListController {
chatListController = c
break
}
}
} else if let c = viewController as? ChatListController {
chatListController = c
break
}
}
if let chatListController {
navigationController.popToRoot(animated: true)
let context = component.context
chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in
guard let context, let chatListController else {
return
}
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
if case .updates = component.subject {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Updated", text: "You have joined \(result.newChatCount) new chats", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current)
} else if result.newChatCount != 0 {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Added", text: "You also joined \(result.newChatCount) chats", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current)
} else {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Folder \(result.title) Added", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current)
}
})
}
}
controller.dismiss()
}, error: { [weak self] error in
guard let self, let component = self.component, let controller = self.environment?.controller() else { guard let self, let component = self.component, let controller = self.environment?.controller() else {
return return
} }
@ -772,11 +855,6 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
controller.push(limitController) controller.push(limitController)
controller.dismiss() controller.dismiss()
} }
}, completed: { [weak self] in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss()
}) })
} else { } else {
controller.dismiss() controller.dismiss()
@ -796,6 +874,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
transition.setFrame(view: actionButtonView, frame: actionButtonFrame) 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(
size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset), size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset),
@ -817,16 +898,16 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset) let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 10.0 //self.scrollContentClippingView.layer.cornerRadius = 10.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
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 - 24.0 - (containerInset + 56.0))) 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)))
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))
@ -876,6 +957,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false self.automaticallyControlPresentationContextLayout = false
self.lockOrientation = true
} }
required public init(coder aDecoder: NSCoder) { required public init(coder aDecoder: NSCoder) {

View File

@ -182,7 +182,7 @@ final class PeerListItemComponent: Component {
if themeUpdated { if themeUpdated {
var theme = CheckNodeTheme(theme: component.theme, style: .plain) var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted { if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35) theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
} }
checkLayer.theme = theme checkLayer.theme = theme
} }
@ -190,7 +190,7 @@ final class PeerListItemComponent: Component {
} else { } else {
var theme = CheckNodeTheme(theme: component.theme, style: .plain) var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted { if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35) theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
} }
checkLayer = CheckLayer(theme: theme) checkLayer = CheckLayer(theme: theme)
self.checkLayer = checkLayer self.checkLayer = checkLayer

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "link_18.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,101 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.304932 1.549805 cm
0.000000 0.000000 0.000000 scn
7.766377 12.077935 m
8.749319 13.060877 10.342982 13.060877 11.325925 12.077935 c
12.308867 11.094994 12.308867 9.501329 11.325925 8.518387 c
9.825925 7.018387 l
8.842983 6.035446 7.249319 6.035446 6.266377 7.018387 c
6.135469 7.149295 6.022435 7.290437 5.926912 7.438976 c
5.728258 7.747883 5.316799 7.837261 5.007892 7.638608 c
4.698985 7.439953 4.609607 7.028494 4.808261 6.719588 c
4.954801 6.491719 5.127476 6.276385 5.325925 6.077936 c
6.828264 4.575597 9.264038 4.575597 10.766377 6.077936 c
12.266377 7.577936 l
13.768716 9.080275 13.768716 11.516048 12.266377 13.018387 c
10.764037 14.520726 8.328264 14.520726 6.825925 13.018387 c
5.325925 11.518387 l
5.066226 11.258689 5.066226 10.837634 5.325925 10.577935 c
5.585624 10.318236 6.006679 10.318236 6.266377 10.577935 c
7.766377 12.077935 l
h
5.626755 2.818445 m
4.643812 1.835504 3.050149 1.835504 2.067207 2.818445 c
1.084265 3.801387 1.084265 5.395051 2.067207 6.377992 c
3.567207 7.877992 l
4.550149 8.860934 6.143812 8.860934 7.126754 7.877992 c
7.257662 7.747084 7.370696 7.605942 7.466219 7.457404 c
7.664873 7.148497 8.076332 7.059119 8.385240 7.257772 c
8.694146 7.456426 8.783525 7.867885 8.584870 8.176792 c
8.438332 8.404661 8.265656 8.619995 8.067206 8.818444 c
6.564867 10.320784 4.129094 10.320784 2.626755 8.818444 c
1.126755 7.318444 l
-0.375585 5.816106 -0.375585 3.380332 1.126755 1.877994 c
2.629094 0.375654 5.064867 0.375654 6.567206 1.877994 c
8.067206 3.377994 l
8.326905 3.637691 8.326905 4.058746 8.067206 4.318445 c
7.807508 4.578144 7.386453 4.578144 7.126754 4.318445 c
5.626755 2.818445 l
h
f*
n
Q
endstream
endobj
3 0 obj
1707
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 18.000000 18.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001797 00000 n
0000001820 00000 n
0000001993 00000 n
0000002067 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2126
%%EOF

View File

@ -7292,7 +7292,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self, case let .message(index) = toIndex { if let strongSelf = self, case let .message(index) = toIndex {
if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id {
if messageId.peerId == index.id.peerId { if messageId.peerId == index.id.peerId {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist), elevatedLayout: false, action: { _ in return true }), in: .current) strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
} }
} else if let controllerInteraction = strongSelf.controllerInteraction { } else if let controllerInteraction = strongSelf.controllerInteraction {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) {
@ -8771,12 +8771,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
bannedMediaInput = true bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil { } else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo { if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return return
} }
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil { } else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo { if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return return
} }
} }
@ -8785,12 +8785,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
bannedMediaInput = true bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) { } else if group.hasBannedPermission(.banSendVoice) {
if !isVideo { if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return return
} }
} else if group.hasBannedPermission(.banSendInstantVideos) { } else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo { if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return return
} }
} }
@ -10092,7 +10092,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable) presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable)
}, presentGigagroupHelp: { [weak self] in }, presentGigagroupHelp: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription), elevatedLayout: false, action: { _ in return true }), in: .current) strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
} }
}, editMessageMedia: { [weak self] messageId, draw in }, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self { if let strongSelf = self {
@ -11166,7 +11166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: {
let context = strongSelf.context let context = strongSelf.context
@ -12959,7 +12959,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false return false
} }
}), case let .app(_, botName, _) = button { }), case let .app(_, botName, _) = button {
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
} else { } else {
let _ = (context.engine.messages.getAttachMenuBot(botId: botId) let _ = (context.engine.messages.getAttachMenuBot(botId: botId)
|> deliverOnMainQueue).start(next: { bot in |> deliverOnMainQueue).start(next: { bot in

View File

@ -815,7 +815,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start() let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start()
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip) let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip, timeout: nil)
controllerInteraction.displayUndo(content) controllerInteraction.displayUndo(content)
}), false), at: 0) }), false), at: 0)
actions.insert(.separator, at: 1) actions.insert(.separator, at: 1)
@ -842,9 +842,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 }))
if size > settings.maxSize { if size > settings.maxSize {
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)) controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil))
} else if Double(duration) > Double(settings.maxDuration) { } else if Double(duration) > Double(settings.maxDuration) {
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil))
} else { } else {
let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file))
|> deliverOnMainQueue).start(completed: { |> deliverOnMainQueue).start(completed: {

View File

@ -164,7 +164,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: new, stickerPacks: [new], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: new, stickerPacks: [new], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil)
return true return true
} }
case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite), let .participantJoinByRequest(invite, _): case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite, _), let .participantJoinByRequest(invite, _):
if let inviteLink = invite.link, !inviteLink.hasSuffix("...") { if let inviteLink = invite.link, !inviteLink.hasSuffix("...") {
if invite.isPermanent { if invite.isPermanent {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)

View File

@ -1418,7 +1418,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil)
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
case let .participantJoinedViaInvite(invite): case let .participantJoinedViaInvite(invite, joinedViaFolderLink):
var peers = SimpleDictionary<PeerId, Peer>() var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer? var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] { if let peer = self.entry.peers[self.entry.event.peerId] {
@ -1429,7 +1429,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = "" var text: String = ""
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") let rawText: PresentationStrings.FormattedString
if joinedViaFolderLink {
rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "")
} else {
rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "")
}
appendAttributedText(text: rawText, generateEntities: { index in appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author { if index == 0, let author = author {

View File

@ -593,7 +593,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} }
case let .startAttach(peerId, payload, choose): case let .startAttach(peerId, payload, choose):
let presentError: (String) -> Void = { errorText in let presentError: (String) -> Void = { errorText in
present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in
return true return true
}), nil) }), nil)
} }

View File

@ -6080,7 +6080,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|> deliverOnMainQueue).start(completed: { [weak self] in |> deliverOnMainQueue).start(completed: { [weak self] in
if let strongSelf = self, let peer = strongSelf.data?.peer { if let strongSelf = self, let peer = strongSelf.data?.peer {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })
controller.keepOnParentDismissal = true controller.keepOnParentDismissal = true
strongSelf.controller?.present(controller, in: .window(.root)) strongSelf.controller?.present(controller, in: .window(.root))

View File

@ -13,7 +13,6 @@ swift_library(
"//submodules/TelegramCore:TelegramCore", "//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox:Postbox", "//submodules/Postbox:Postbox",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -3,7 +3,12 @@ import UIKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import Display
private extension UIColor {
convenience init(rgb: UInt32) {
self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0)
}
}
public enum PresentationBuiltinThemeReference: Int32 { public enum PresentationBuiltinThemeReference: Int32 {
case dayClassic = 0 case dayClassic = 0

View File

@ -12,7 +12,7 @@ public enum UndoOverlayContent {
case hidArchive(title: String, text: String, undo: Bool) case hidArchive(title: String, text: String, undo: Bool)
case revealedArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool)
case succeed(text: String) case succeed(text: String)
case info(title: String?, text: String) case info(title: String?, text: String, timeout: Double?)
case emoji(name: String, text: String) case emoji(name: String, text: String)
case swipeToReply(title: String, text: String) case swipeToReply(title: String, text: String)
case actionSucceeded(title: String, text: String, cancel: String) case actionSucceeded(title: String, text: String, cancel: String)

View File

@ -173,7 +173,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.maximumNumberOfLines = 5 self.textNode.maximumNumberOfLines = 5
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 3 self.originalRemainingSeconds = 3
case let .info(title, text): case let .info(title, text, timeout):
self.avatarNode = nil self.avatarNode = nil
self.iconNode = nil self.iconNode = nil
self.iconCheckNode = nil self.iconCheckNode = nil
@ -193,11 +193,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 10 self.textNode.maximumNumberOfLines = 10
displayUndo = false displayUndo = false
if let timeout {
self.originalRemainingSeconds = timeout
} else {
self.originalRemainingSeconds = Double(max(5, min(8, text.count / 14))) self.originalRemainingSeconds = Double(max(5, min(8, text.count / 14)))
}
if text.contains("](") { if text.contains("](") {
isUserInteractionEnabled = true isUserInteractionEnabled = true
} }
case let .actionSucceeded(title, text, cancel): case let .actionSucceeded(title, text, cancel):
self.avatarNode = nil self.avatarNode = nil
self.iconNode = nil self.iconNode = nil