Merge commit '2266e38d691b345f49ae3c2cf9cc669c808b31d6'

This commit is contained in:
Ali 2021-01-20 00:27:52 +04:00
commit 347cc10a6e
42 changed files with 5746 additions and 4756 deletions

View File

@ -5845,6 +5845,7 @@ Sorry for the inconvenience.";
"InviteLink.Expired" = "expired";
"InviteLink.UsageLimitReached" = "limit reached";
"InviteLink.Revoked" = "revoked";
"InviteLink.TapToCopy" = "tap to copy";
"InviteLink.AdditionalLinks" = "Additional Links";
"InviteLink.Create" = "Create a New Link";
@ -5885,10 +5886,17 @@ Sorry for the inconvenience.";
"InviteLink.InviteLink" = "Invite Link";
"InviteLink.CreatedBy" = "Link Created By";
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links";
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete";
"InviteLink.DeleteLinkAlert.Text" = "Are you sure you want to delete this link? It will be completely gone.";
"InviteLink.DeleteLinkAlert.Action" = "Delete";
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links.";
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete All";
"InviteLink.ExpiresIn" = "expires in %@";
"Conversation.ChecksTooltip.Delivered" = "Delivered";
"Conversation.ChecksTooltip.Read" = "Read";
"DialogList.MultipleTypingPair" = "%@ and %@ are typing";
"Common.Save" = "Save";

View File

@ -1212,8 +1212,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
switch controller.content {
case let .archivedChat(archivedChat):
if peerIds.contains(PeerId(archivedChat.peerId)) {
case let .archivedChat(peerId, _, _, _):
if peerIds.contains(PeerId(peerId)) {
controller.dismiss()
}
default:

View File

@ -158,7 +158,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
interaction.activateSearch()
})
case let .sort(theme, strings, sortOrder):
case let .sort(_, strings, sortOrder):
var text = strings.Contacts_SortedByName
if case .presence = sortOrder {
text = strings.Contacts_SortedByPresence
@ -166,17 +166,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
interaction.openSortMenu()
})
case let .permissionInfo(theme, title, text, suppressed):
case let .permissionInfo(_, title, text, suppressed):
return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
interaction.suppressWarning()
})
case let .permissionEnable(theme, text):
case let .permissionEnable(_, text):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: {
interaction.authorize()
})
case let .option(_, option, header, theme, _):
case let .option(_, option, header, _, _):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action)
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
var status: ContactsPeerItemStatus
let itemPeer: ContactsPeerItemPeer
var isContextActionEnabled = false
@ -928,9 +928,9 @@ public final class ContactListNode: ASDisplayNode {
|> mapToSignal { presentation in
var generateSections = false
var includeChatList = false
if case let .natural(natural) = presentation {
if case let .natural(_, includeChatListValue) = presentation {
generateSections = true
includeChatList = natural.includeChatList
includeChatList = includeChatListValue
}
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {

View File

@ -1225,7 +1225,7 @@ public class GalleryController: ViewController, StandalonePresentableController
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
if let (media, _) = mediaForMessage(message: message) {
if let _ = mediaForMessage(message: message) {
centralItemNode.activateAsInitial()
}
}

View File

@ -50,7 +50,7 @@ func isValidNumberOfUsers(_ number: String) -> Bool {
private enum InviteLinksEditEntry: ItemListNodeEntry {
case timeHeader(PresentationTheme, String)
case timePicker(PresentationTheme, InviteLinkTimeLimit)
case timeExpiryDate(PresentationTheme, Int32?)
case timeExpiryDate(PresentationTheme, Int32?, Bool)
case timeCustomPicker(PresentationTheme, Int32?)
case timeInfo(PresentationTheme, String)
@ -111,8 +111,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else {
return false
}
case let .timeExpiryDate(lhsTheme, lhsDate):
if case let .timeExpiryDate(rhsTheme, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate {
case let .timeExpiryDate(lhsTheme, lhsDate, lhsActive):
if case let .timeExpiryDate(rhsTheme, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate, lhsActive == rhsActive {
return true
} else {
return false
@ -182,14 +182,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState
})
})
case let .timeExpiryDate(_, value):
case let .timeExpiryDate(theme, value, active):
let text: String
if let value = value {
text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."))
} else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
}
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.dismissInput()
arguments.updateState { state in
var updatedState = state
@ -223,7 +223,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
})
case let .usageCustomPicker(theme, value, focused):
let text = value.flatMap { String($0) } ?? (focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited)
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, tag: nil, sectionId: self.section, textUpdated: { updatedText in
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, tag: nil, sectionId: self.section, textUpdated: { updatedText in
guard !updatedText.isEmpty else {
return
}
@ -276,7 +276,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
} else if let value = state.time.value {
time = currentTime + value
}
entries.append(.timeExpiryDate(presentationData.theme, time))
entries.append(.timeExpiryDate(presentationData.theme, time, state.pickingTimeLimit))
if state.pickingTimeLimit {
entries.append(.timeCustomPicker(presentationData.theme, time ?? currentTime))
}
@ -300,9 +300,10 @@ private struct InviteLinkEditControllerState: Equatable {
var time: InviteLinkTimeLimit
var pickingTimeLimit = false
var pickingUsageLimit = false
var updating = false
}
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: (() -> Void)? = nil) -> ViewController {
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet()
@ -327,7 +328,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false)
} else {
initialState = InviteLinkEditControllerState(usage: .medium, time: .week, pickingTimeLimit: false, pickingUsageLimit: false)
initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, pickingTimeLimit: false, pickingUsageLimit: false)
}
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -359,8 +360,17 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
dismissAction()
dismissImpl?()
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
completion?()
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link)
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in
completion?(invite)
}, error: { _ in
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
})
]),
@ -369,6 +379,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
presentControllerImpl?(controller, nil)
})
let previousState = Atomic<InviteLinkEditControllerState?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
@ -376,7 +387,13 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
dismissImpl?()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: {
updateState { state in
var updatedState = state
updatedState.updating = true
return updatedState
}
let expireDate: Int32?
if case let .custom(value) = state.time {
expireDate = value
@ -390,21 +407,43 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
let usageLimit = state.usage.value
if invite == nil {
let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|> deliverOnMainQueue).start(next: { result in
completion?()
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in
completion?(invite)
dismissImpl?()
}, error: { _ in
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
} else if let invite = invite {
let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|> deliverOnMainQueue).start(next: { result in
completion?()
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in
completion?(invite)
dismissImpl?()
}, error: { _ in
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
}
})
let previousState = previousState.swap(state)
var animateChanges = false
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit {
animateChanges = true
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}

View File

@ -45,25 +45,39 @@ private struct InviteLinkInviteTransaction {
}
private enum InviteLinkInviteEntryId: Hashable {
case header
case mainLink
case links(Int32)
case manage
}
private enum InviteLinkInviteEntry: Comparable, Identifiable {
case header(PresentationTheme, String, String)
case mainLink(PresentationTheme, ExportedInvitation)
case links(Int32, PresentationTheme, [ExportedInvitation])
case manage(PresentationTheme, String, Bool)
var stableId: InviteLinkInviteEntryId {
switch self {
case .header:
return .header
case .mainLink:
return .mainLink
case let .links(index, _, _):
return .links(index)
case .manage:
return .manage
}
}
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsTitle, lhsText):
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .mainLink(lhsTheme, lhsInvitation):
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
return true
@ -76,43 +90,73 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
} else {
return false
}
case let .manage(lhsTheme, lhsText, lhsStandalone):
if case let .manage(rhsTheme, rhsText, rhsStandalone) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStandalone == rhsStandalone {
return true
} else {
return false
}
}
}
static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case .header:
switch rhs {
case .header:
return false
case .mainLink, .links, .manage:
return true
}
case .mainLink:
switch rhs {
case .mainLink:
case .header, .mainLink:
return false
case .links:
case .links, .manage:
return true
}
case let .links(lhsIndex, _, _):
switch rhs {
case .mainLink:
case .header, .mainLink:
return false
case let .links(rhsIndex, _, _):
return lhsIndex < rhsIndex
case .manage:
return true
}
case .manage:
switch rhs {
case .header, .mainLink, .links:
return false
case .manage:
return true
}
}
}
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
switch self {
case let .header(theme, title, text):
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
case let .mainLink(_, invite):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: nil, sectionId: 0, style: .plain, shareAction: {
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
interaction.copyLink(invite)
}, shareAction: {
interaction.shareLink(invite)
}, contextAction: { node in
interaction.mainLinkContextAction(invite, node, nil)
}, viewAction: {
})
case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, sectionId: 0, style: .plain, tapAction: { invite in
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, share: true, sectionId: 1, style: .plain, tapAction: { invite in
interaction.copyLink(invite)
}, contextAction: { invite, _ in
interaction.shareLink(invite)
})
case let .manage(theme, text, standalone):
return InviteLinkInviteManageItem(theme: theme, text: text, standalone: standalone, action: {
interaction.manageLinks()
})
}
}
}
@ -136,13 +180,14 @@ public final class InviteLinkInviteController: ViewController {
private let context: AccountContext
private let peerId: PeerId
private weak var parentNavigationController: NavigationController?
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId) {
fatalError()
public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) {
self.context = context
self.peerId = peerId
self.parentNavigationController = parentNavigationController
super.init(navigationBarPresentationData: nil)
@ -196,7 +241,7 @@ public final class InviteLinkInviteController: ViewController {
self.controllerNode.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
self?.presentingViewController?.dismiss(animated: false, completion: nil)
})
}
}
@ -212,6 +257,7 @@ public final class InviteLinkInviteController: ViewController {
private let context: AccountContext
private let peerId: PeerId
private let invitesContext: PeerExportedInvitationsContext
private var interaction: InviteLinkInviteInteraction?
@ -244,6 +290,8 @@ public final class InviteLinkInviteController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise = Promise(self.presentationData)
self.controller = controller
self.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
@ -328,7 +376,7 @@ public final class InviteLinkInviteController: ViewController {
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
self?.revokeDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
self?.revokeDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
}))
})
@ -341,31 +389,53 @@ public final class InviteLinkInviteController: ViewController {
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
self?.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root))
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
}, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root))
}, manageLinks: { [weak self] in
let controller = inviteLinkListController(context: context, peerId: peerId)
self?.controller?.push(controller)
self?.controller?.parentNavigationController?.pushViewController(controller)
self?.controller?.dismiss()
})
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
let peerView = context.account.postbox.peerView(id: peerId)
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view in
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, self.invitesContext.state)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in
if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
let mainInvite: ExportedInvitation?
if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
entries.append(.mainLink(presentationData.theme, invite))
mainInvite = invite
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
entries.append(.mainLink(presentationData.theme, invite))
mainInvite = invite
} else {
mainInvite = nil
}
if let mainInvite = mainInvite {
entries.append(.mainLink(presentationData.theme, mainInvite))
}
let additionalInvites = invites.invitations.filter { $0.link != mainInvite?.link }
var index: Int32 = 0
for i in stride(from: 0, to: additionalInvites.endIndex, by: 2) {
var invitesPair: [ExportedInvitation] = []
invitesPair.append(additionalInvites[i])
if i + 1 < additionalInvites.count {
invitesPair.append(additionalInvites[i + 1])
}
entries.append(.links(index, presentationData.theme, invitesPair))
index += 1
}
entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage, additionalInvites.isEmpty))
let previousEntries = previousEntries.swap(entries)
@ -507,14 +577,14 @@ public final class InviteLinkInviteController: ViewController {
insets.bottom = layout.intrinsicInsets.bottom
let headerHeight: CGFloat = 54.0
let visibleItemsHeight: CGFloat = 147.0 + floor(52.0 * 3.5)
let visibleItemsHeight: CGFloat = 409.0
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let listTopInset = layoutTopInset + headerHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight)
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight - insets.bottom)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve)
@ -539,11 +609,6 @@ public final class InviteLinkInviteController: ViewController {
if result === self.headerNode.view {
return self.view
}
if result === self.headerNode.view {
return self.view
}
if !self.bounds.contains(point) {
return nil
}
@ -569,8 +634,6 @@ public final class InviteLinkInviteController: ViewController {
case .changed:
var translation = recognizer.translation(in: self.contentNode.view).y
if let currentPanOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 {
} else {
translation = currentPanOffset

View File

@ -13,10 +13,12 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0
let theme: PresentationTheme
let title: String
let text: String
init(theme: PresentationTheme, text: String) {
init(theme: PresentationTheme, title: String, text: String) {
self.theme = theme
self.title = title
self.text = text
}
@ -57,58 +59,86 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
}
}
private let titleFont = Font.regular(13.0)
private let titleFont = Font.medium(23.0)
private let textFont = Font.regular(13.0)
class InviteLinkInviteHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var animationNode: AnimatedStickerNode
private let textNode: TextNode
private let iconBackgroundNode: ASImageNode
private let iconNode: ASImageNode
private var item: InviteLinkInviteHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.iconBackgroundNode = ASImageNode()
self.iconBackgroundNode.displaysAsynchronously = false
self.iconBackgroundNode.displayWithoutProcessing = true
self.iconNode = ASImageNode()
self.iconNode.contentMode = .center
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.addSubnode(self.iconBackgroundNode)
self.addSubnode(self.iconNode)
}
func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0
let leftInset: CGFloat = 40.0 + params.leftInset
let topInset: CGFloat = 98.0
let spacing: CGFloat = 8.0
let bottomInset: CGFloat = 24.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors)
let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + spacing + textLayout.size.height + bottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
if let _ = updatedTheme {
strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 92.0, color: item.theme.actionSheet.controlAccentColor)
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/LargeLink"), color: item.theme.list.itemCheckColors.foregroundColor)
}
let iconSize = CGSize(width: 96.0, height: 96.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let iconSize = CGSize(width: 92.0, height: 92.0)
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 8.0 + titleLayout.size.height + spacing), size: textLayout.size)
}
})
}

View File

@ -0,0 +1,118 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class InviteLinkInviteManageItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0
let theme: PresentationTheme
let text: String
let standalone: Bool
let action: () -> Void
init(theme: PresentationTheme, text: String, standalone: Bool, action: @escaping () -> Void) {
self.theme = theme
self.text = text
self.standalone = standalone
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = InviteLinkInviteManageItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? InviteLinkInviteManageItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.medium(23.0)
private let textFont = Font.regular(13.0)
class InviteLinkInviteManageItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private var item: InviteLinkInviteManageItem?
init() {
self.backgroundNode = ASDisplayNode()
self.buttonNode = HighlightableButtonNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.item?.action()
}
func asyncLayout() -> (_ item: InviteLinkInviteManageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize = CGSize(width: params.width, height: 70.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.backgroundNode.backgroundColor = item.standalone ? .clear : item.theme.list.blocksBackgroundColor
strongSelf.buttonNode.setTitle(item.text, with: Font.regular(17.0), with: item.theme.actionSheet.controlAccentColor, for: .normal)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: 1000.0))
let size = strongSelf.buttonNode.measure(layout.contentSize)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.contentSize.width - size.width) / 2.0), y: floorToScreenPixels((layout.contentSize.height - size.height) / 2.0)), size: size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -22,18 +22,20 @@ import ShareController
private final class InviteLinkListControllerArguments {
let context: AccountContext
let shareMainLink: (ExportedInvitation?) -> Void
let openMainLink: (ExportedInvitation?) -> Void
let shareMainLink: (ExportedInvitation) -> Void
let openMainLink: (ExportedInvitation) -> Void
let copyLink: (ExportedInvitation) -> Void
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let createLink: () -> Void
let openLink: (ExportedInvitation) -> Void
let linkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let deleteAllRevokedLinks: () -> Void
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation?) -> Void, openMainLink: @escaping (ExportedInvitation?) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation) -> Void, openMainLink: @escaping (ExportedInvitation) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
self.context = context
self.shareMainLink = shareMainLink
self.openMainLink = openMainLink
self.copyLink = copyLink
self.mainLinkContextAction = mainLinkContextAction
self.createLink = createLink
self.openLink = openLink
@ -178,8 +180,14 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case let .mainLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .mainLink(_, invite, peers):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
arguments.shareMainLink(invite)
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, displayButton: true, displayImporters: true, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
if let invite = invite {
arguments.copyLink(invite)
}
}, shareAction: {
if let invite = invite {
arguments.shareMainLink(invite)
}
}, contextAction: { node in
arguments.mainLinkContextAction(invite, node, nil)
}, viewAction: {
@ -194,7 +202,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.createLink()
})
case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite)
}, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil)
@ -208,7 +216,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.deleteAllRevokedLinks()
})
case let .revokedLinks(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite)
}, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil)
@ -298,34 +306,22 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
let deleteAllRevokedLinksDisposable = MetaDisposable()
actionsDisposable.add(deleteAllRevokedLinksDisposable)
actionsDisposable.add((context.account.viewTracker.peerView(peerId) |> filter { $0.cachedData != nil } |> take(1) |> mapToSignal { view -> Signal<String?, NoError> in
return ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId)
|> mapToSignal { _ -> Signal<String?, NoError> in
return .complete()
}
}).start())
var getControllerImpl: (() -> ViewController?)?
let invitesPromise = Promise<ExportedInvitations?>()
invitesPromise.set(.single(nil)
|> then(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false)))
let revokedInvitesPromise = Promise<ExportedInvitations?>()
revokedInvitesPromise.set(.single(nil)
|> then(peerExportedInvitations(account: context.account, peerId: peerId, revoked: true)))
let invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
let revokedInvitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: true, forceUpdate: true)
let arguments = InviteLinkListControllerArguments(context: context, shareMainLink: { invite in
if let invite = invite {
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
}
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
}, openMainLink: { invite in
if let invite = invite {
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil)
pushControllerImpl?(controller)
}
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
pushControllerImpl?(controller)
}, copyLink: { invite in
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
}, mainLinkContextAction: { invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
return
@ -383,13 +379,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
}
}
if revoke {
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
updateState { state in
var updatedState = state
updatedState.revokingPrivateLink = false
return updatedState
}
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
invitesContext.reload()
revokedInvitesContext.reload()
}))
}
})
@ -402,14 +400,16 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
}, createLink: {
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: {
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: { invite in
if let invite = invite {
invitesContext.add(invite)
}
})
controller.navigationPresentation = .modal
pushControllerImpl?(controller)
}, openLink: { invite in
if let invite = invite {
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil)
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: invitesContext, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
pushControllerImpl?(controller)
}
}, linkContextAction: { invite, node, gesture in
@ -445,8 +445,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
}, action: { _, f in
f(.dismissWithoutContent)
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: {
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: { invite in
if let invite = invite {
if invite.isRevoked {
invitesContext.remove(invite)
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
} else {
invitesContext.update(invite)
}
}
})
controller.navigationPresentation = .modal
pushControllerImpl?(controller)
@ -465,13 +472,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
ActionSheetTextItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
dismissAction()
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
revokeLinkDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
revokedInvitesContext.remove(invite)
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -497,6 +506,9 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
invitesContext.remove(invite)
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -520,8 +532,9 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
dismissAction()
deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
}))
revokedInvitesContext.clear()
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -551,11 +564,11 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
importersState.set(context.state |> map(Optional.init))
}
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesPromise.get(), revokedInvitesPromise.get())
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state)
|> deliverOnMainQueue
|> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.InviteLink_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites?.list, revokedInvites: revokedInvites?.list, mainPeers: importers?.importers.compactMap { $0.peer.peer } ?? []), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites.invitations, revokedInvites: revokedInvites.invitations, mainPeers: importers?.importers.compactMap { $0.peer.peer } ?? []), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
return (controllerState, (listState, arguments))
}

View File

@ -209,7 +209,7 @@ public final class InviteLinkQRCodeController: ViewController {
self.contentContainerNode.addSubnode(self.qrIconNode)
self.contentContainerNode.addSubnode(self.qrButtonNode)
let textFont = Font.regular(16.0)
let textFont = Font.regular(13.0)
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Info, font: textFont, textColor: secondaryTextColor)
self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share
@ -342,7 +342,6 @@ public final class InviteLinkQRCodeController: ViewController {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input])
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
let makeImageLayout = self.qrImageNode.asyncLayout()
@ -352,7 +351,7 @@ public final class InviteLinkQRCodeController: ViewController {
let _ = imageApply()
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 24.0), size: imageSize)
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 16.0), size: imageSize)
transition.updateFrame(node: self.qrImageNode, frame: imageFrame)
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
@ -365,7 +364,7 @@ public final class InviteLinkQRCodeController: ViewController {
let inset: CGFloat = 22.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxX + 20.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxY + 20.0), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let buttonSideInset: CGFloat = 16.0
@ -379,7 +378,7 @@ public final class InviteLinkQRCodeController: ViewController {
let titleHeight: CGFloat = 54.0
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 52.0 + 77.0
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 121.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
@ -401,7 +400,7 @@ public final class InviteLinkQRCodeController: ViewController {
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 16.0), size: cancelSize)
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 18.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0

View File

@ -24,12 +24,14 @@ import DirectionalPanGesture
class InviteLinkViewInteraction {
let context: AccountContext
let openPeer: (PeerId) -> Void
let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void
let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void
init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) {
init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) {
self.context = context
self.openPeer = openPeer
self.copyLink = copyLink
self.shareLink = shareLink
self.contextAction = contextAction
}
@ -72,7 +74,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case creatorHeader(PresentationTheme, String)
case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
case importerHeader(PresentationTheme, String)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32, Bool)
var stableId: InviteLinkViewEntryId {
switch self {
@ -84,7 +86,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
return .creator
case .importerHeader:
return .importerHeader
case let .importer(_, _, _, peer, _):
case let .importer(_, _, _, peer, _, _):
return .importer(peer.id)
}
}
@ -115,8 +117,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else {
return false
}
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsLoading == rhsLoading {
return true
} else {
return false
@ -154,11 +156,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case .creator, .importer:
return true
}
case let .importer(lhsIndex, _, _, _, _):
case let .importer(lhsIndex, _, _, _, _, _):
switch rhs {
case .link, .creatorHeader, .creator, .importerHeader:
return false
case let .importer(rhsIndex, _, _, _, _):
case let .importer(rhsIndex, _, _, _, _, _):
return lhsIndex < rhsIndex
}
}
@ -168,7 +170,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
switch self {
case let .link(_, invite):
let buttonColor = color(for: invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: buttonColor, sectionId: 0, style: .plain, shareAction: {
let availability = invitationAvailability(invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: !invite.isRevoked && !availability.isZero, displayImporters: false, buttonColor: buttonColor, sectionId: 0, style: .plain, copyAction: {
interaction.copyLink(invite)
}, shareAction: {
interaction.shareLink(invite)
}, contextAction: { node in
interaction.contextAction(invite, node, nil)
@ -183,11 +188,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
case let .importerHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .importer(_, _, dateTimeFormat, peer, date):
case let .importer(_, _, dateTimeFormat, peer, date, loading):
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: true, sectionId: 0, action: {
interaction.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil)
}
}
}
@ -212,14 +217,18 @@ public final class InviteLinkViewController: ViewController {
private let context: AccountContext
private let peerId: PeerId
private let invite: ExportedInvitation
private let invitationsContext: PeerExportedInvitationsContext?
private let revokedInvitationsContext: PeerExportedInvitationsContext?
private let importersContext: PeerInvitationImportersContext?
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?) {
public init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?) {
self.context = context
self.peerId = peerId
self.invite = invite
self.invitationsContext = invitationsContext
self.revokedInvitationsContext = revokedInvitationsContext
self.importersContext = importersContext
super.init(navigationBarPresentationData: nil)
@ -292,12 +301,16 @@ public final class InviteLinkViewController: ViewController {
private let peerId: PeerId
private let invite: ExportedInvitation
private let importersContext: PeerInvitationImportersContext
private var interaction: InviteLinkViewInteraction?
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private var presentationDataDisposable: Disposable?
private var disposable: Disposable?
private let actionDisposable = MetaDisposable()
private let dimNode: ASDisplayNode
private let contentNode: ASDisplayNode
@ -316,10 +329,6 @@ public final class InviteLinkViewController: ViewController {
private var validLayout: ContainerViewLayout?
private var presentationDataDisposable: Disposable?
private let importersContext: PeerInvitationImportersContext
init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?, controller: InviteLinkViewController) {
self.context = context
self.peerId = peerId
@ -382,6 +391,10 @@ public final class InviteLinkViewController: ViewController {
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always))
}
}, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
}, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root))
@ -403,7 +416,33 @@ public final class InviteLinkViewController: ViewController {
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
})))
if !invite.isRevoked {
if invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
dismissAction()
self?.actionDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
self?.controller?.revokedInvitationsContext?.remove(invite)
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self?.controller?.present(controller, in: .window(.root))
})))
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
@ -430,16 +469,23 @@ public final class InviteLinkViewController: ViewController {
entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, invite.date))
if !state.importers.isEmpty {
if !state.importers.isEmpty || (state.isLoadingMore && state.count > 0) {
entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased()))
}
var index: Int32 = 0
for importer in state.importers {
if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date))
if state.importers.isEmpty && state.isLoadingMore {
let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
for i in 0 ..< min(4, state.count) {
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, fakeUser, 0, true))
}
} else {
for importer in state.importers {
if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date, false))
}
index += 1
}
index += 1
}
let previousEntries = previousEntries.swap(entries)
@ -514,8 +560,17 @@ public final class InviteLinkViewController: ViewController {
let navigationController = self.controller?.navigationController as? NavigationController
self.controller?.dismiss()
let invitationsContext = self.controller?.invitationsContext
if let navigationController = navigationController {
let controller = inviteLinkEditController(context: self.context, peerId: self.peerId, invite: self.invite)
let controller = inviteLinkEditController(context: self.context, peerId: self.peerId, invite: self.invite, completion: { [weak self] invite in
if let invite = invite {
if invite.isRevoked {
invitationsContext?.remove(invite)
} else {
invitationsContext?.update(invite)
}
}
})
controller.navigationPresentation = .modal
navigationController.pushViewController(controller)
}
@ -638,11 +693,6 @@ public final class InviteLinkViewController: ViewController {
if result === self.headerNode.view {
return self.view
}
if result === self.headerNode.view {
return self.view
}
if !self.bounds.contains(point) {
return nil
}

View File

@ -42,10 +42,60 @@ func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
availability = min(fraction, availability)
}
return availability
return max(0.0, min(1.0, availability))
}
private enum ItemBackgroundColor: Equatable {
case blue
case green
case yellow
case red
case gray
var colors: (top: UIColor, bottom: UIColor, text: UIColor) {
switch self {
case .blue:
return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff))
case .green:
return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6))
case .yellow:
return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7))
case .red:
return (UIColor(rgb: 0xf2656a), UIColor(rgb: 0xf25f65), UIColor(rgb: 0xffd3de))
case .gray:
return (UIColor(rgb: 0xa8b2bb), UIColor(rgb: 0xa2abb4), UIColor(rgb: 0xe3e6e8))
}
}
}
private let moreIcon = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
})
private let shareIcon = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if let maskImage = UIImage(bundleImageName: "Chat/Links/Share") {
context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - maskImage.size.width) / 2.0), y: floorToScreenPixels((size.height - maskImage.size.height) / 2.0)), size: maskImage.size), mask: maskImage.cgImage!)
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
})
private class ItemNode: ASDisplayNode {
private let selectionNode: HighlightTrackingButtonNode
private let wrapperNode: ASDisplayNode
private let backgroundNode: ASImageNode
private let iconNode: ASImageNode
@ -59,19 +109,28 @@ private class ItemNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData)?
private var updateTimer: SwiftSignalKit.Timer?
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, color: ItemBackgroundColor, presentationData: ItemListPresentationData)?
var action: (() -> Void)?
var contextAction: ((ASDisplayNode) -> Void)?
private let hapticFeedback = HapticFeedback()
override init() {
self.selectionNode = HighlightTrackingButtonNode()
self.wrapperNode = ASDisplayNode()
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.buttonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode()
@ -80,37 +139,44 @@ private class ItemNode: ASDisplayNode {
self.buttonIconNode = ASImageNode()
self.buttonIconNode.displaysAsynchronously = false
self.buttonIconNode.displayWithoutProcessing = true
self.buttonIconNode.image = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
})
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 2
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.backgroundNode)
self.wrapperNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.buttonNode.addSubnode(self.containerNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.wrapperNode.addSubnode(self.selectionNode)
self.wrapperNode.addSubnode(self.buttonNode)
self.wrapperNode.addSubnode(self.titleNode)
self.wrapperNode.addSubnode(self.subtitleNode)
self.selectionNode.addTarget(self, action: #selector(self.tapped), forControlEvents: .touchUpInside)
self.selectionNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear)
transition.updateSublayerTransformScale(node: strongSelf, scale: 0.95)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .linear)
transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
@ -126,11 +192,12 @@ private class ItemNode: ASDisplayNode {
}
}
override func didLoad() {
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
deinit {
self.updateTimer?.invalidate()
}
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
@objc private func tapped() {
self.hapticFeedback.impact(.light)
self.action?()
}
@ -138,35 +205,66 @@ private class ItemNode: ASDisplayNode {
self.contextAction?(self.extractedContainerNode)
}
func update(size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
let updated = self.params?.size != size || self.params?.wide != wide || self.params?.invite != invite
self.params = (size, wide, invite, presentationData)
func update(size: CGSize, wide: Bool, share: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let availability = invitationAvailability(invite)
var isExpired = false
let secondaryTextColor: UIColor
let color: ItemBackgroundColor
if invite.isRevoked {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xd4d8db).cgColor, UIColor(rgb: 0xced2d5).cgColor])
secondaryTextColor = UIColor(rgb: 0xf8f9f9)
color = .gray
} else if invite.expireDate == nil && invite.usageLimit == nil {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x00b5f7).cgColor, UIColor(rgb: 0x00b2f6).cgColor])
secondaryTextColor = UIColor(rgb: 0xa7f4ff)
color = .blue
} else if availability >= 0.5 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x4aca62).cgColor, UIColor(rgb: 0x43c85c).cgColor])
secondaryTextColor = UIColor(rgb: 0xc5ffe6)
color = .green
} else if availability > 0.0 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf8a953).cgColor, UIColor(rgb: 0xf7a64e).cgColor])
secondaryTextColor = UIColor(rgb: 0xfeffd7)
color = .yellow
} else {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf2656a).cgColor, UIColor(rgb: 0xf25f65).cgColor])
secondaryTextColor = UIColor(rgb: 0xffd3de)
isExpired = true
color = .red
}
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0)
let previousParams = self.params
self.params = (size, wide, invite, color, presentationData)
let previousExpireDate = previousParams?.invite.expireDate
if previousExpireDate != invite.expireDate {
self.updateTimer?.invalidate()
self.updateTimer = nil
if let _ = invite.expireDate, availability > 0.0 {
let updateTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
if let (size, wide, invite, _, presentationData) = strongSelf.params {
let _ = strongSelf.update(size: size, wide: wide, share: share, invite: invite, presentationData: presentationData, transition: .animated(duration: 0.3, curve: .linear))
}
}
}, queue: Queue.mainQueue())
self.updateTimer = updateTimer
updateTimer.start()
}
} else if availability.isZero {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
let colors: NSArray = [color.colors.top.cgColor, color.colors.bottom.cgColor]
if let (_, _, previousInvite, previousColor, _) = previousParams, previousInvite == invite {
if previousColor != color {
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
snapshotView.frame = self.wrapperNode.bounds
self.wrapperNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.backgroundNode.image = generateBackgroundImage(colors: colors)
}
} else {
self.backgroundNode.image = generateBackgroundImage(colors: colors)
}
let secondaryTextColor = color.colors.text
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0)
var inviteLink = invite.link.replacingOccurrences(of: "https://", with: "")
if !wide {
inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n")
@ -180,11 +278,13 @@ private class ItemNode: ASDisplayNode {
}
self.titleNode.attributedText = title
self.buttonIconNode.image = share ? shareIcon : moreIcon
var subtitleText: String = ""
if let count = invite.count {
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
} else {
subtitleText = isExpired || invite.isRevoked ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone
subtitleText = [.red, .gray].contains(color) ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone
}
if invite.isRevoked {
if !subtitleText.isEmpty {
@ -194,11 +294,27 @@ private class ItemNode: ASDisplayNode {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
} else if let expireDate = invite.expireDate, currentTime > expireDate {
} else if let expireDate = invite.expireDate, currentTime >= expireDate {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += presentationData.strings.InviteLink_Expired
if share {
subtitleText = presentationData.strings.InviteLink_Expired
} else {
subtitleText += presentationData.strings.InviteLink_Expired
}
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
} else if let usageLimit = invite.usageLimit, let count = invite.count, count >= usageLimit {
if !subtitleText.isEmpty {
subtitleText += ""
}
if share {
subtitleText = presentationData.strings.InviteLink_UsageLimitReached
} else {
subtitleText += presentationData.strings.InviteLink_UsageLimitReached
}
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
@ -209,21 +325,27 @@ private class ItemNode: ASDisplayNode {
timerNode = current
} else {
timerNode = TimerNode()
timerNode.isUserInteractionEnabled = false
self.timerNode = timerNode
self.addSubnode(timerNode)
}
timerNode.update(color: UIColor.white, creationTimestamp: invite.date, deadlineTimestamp: expireDate)
if share {
subtitleText = presentationData.strings.InviteLink_TapToCopy
}
} else {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
if share {
subtitleText = presentationData.strings.InviteLink_TapToCopy
}
}
self.iconNode.frame = CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0)
self.timerNode?.frame = CGRect(x: 8.0, y: 8.0, width: 34.0, height: 34.0)
let subtitle: NSMutableAttributedString = NSMutableAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
self.subtitleNode.attributedText = subtitle
self.subtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
@ -234,7 +356,9 @@ private class ItemNode: ASDisplayNode {
let itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: itemSize)
transition.updateFrame(node: self.wrapperNode, frame: backgroundFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.selectionNode, frame: backgroundFrame)
let buttonSize = CGSize(width: 26.0, height: 26.0)
let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize)
@ -260,7 +384,7 @@ class InviteLinksGridNode: ASDisplayNode {
return result
}
func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], share: Bool, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
self.items = items
var contentSize: CGSize = size
@ -288,7 +412,7 @@ class InviteLinksGridNode: ASDisplayNode {
let col = CGFloat(i % 2)
let row = floor(CGFloat(i) / 2.0)
let wide = (i == self.items.count - 1 && (self.items.count % 2) != 0)
let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, invite: invite, presentationData: presentationData, transition: transition)
let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, share: share, invite: invite, presentationData: presentationData, transition: transition)
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 4.0 + row * (122.0 + itemSpacing)), size: itemSize)
if !wide && col > 0 {
itemFrame.origin.x += itemSpacing + itemSize.width
@ -408,7 +532,7 @@ private final class TimerNode: ASDisplayNode {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp)
fraction = 1.0 - max(0.0, min(0.94, fraction))
fraction = 1.0 - max(0.0, min(1.0, fraction))
let image: UIImage?
@ -424,44 +548,47 @@ private final class TimerNode: ASDisplayNode {
let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime {
removeIndices.append(i)
} else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position
let d = self.particles[i].direction
let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
let sparks = fraction > 0.1 && fraction != 1.0
if sparks {
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime {
removeIndices.append(i)
} else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position
let d = self.particles[i].direction
let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
}
}
}
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
}
}
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in
@ -476,10 +603,12 @@ private final class TimerNode: ASDisplayNode {
context.addPath(path)
context.strokePath()
for particle in self.particles {
let size: CGFloat = 2.0
context.setAlpha(particle.alpha)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
if sparks {
for particle in self.particles {
let size: CGFloat = 2.0
context.setAlpha(particle.alpha)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
}
})

View File

@ -73,7 +73,7 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let datePicker: UIDatePicker
private var datePicker: UIDatePicker?
private var item: ItemListDatePickerItem?
@ -98,25 +98,31 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.datePicker = UIDatePicker()
self.datePicker.minimumDate = Date()
self.datePicker.datePickerMode = .dateAndTime
if #available(iOS 14.0, *) {
self.datePicker.preferredDatePickerStyle = .inline
}
super.init(layerBacked: false, dynamicBounce: false)
self.datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func didLoad() {
super.didLoad()
self.view.addSubview(self.datePicker)
let datePicker = UIDatePicker()
datePicker.minimumDate = Date()
datePicker.datePickerMode = .dateAndTime
if #available(iOS 14.0, *) {
datePicker.preferredDatePickerStyle = .inline
}
datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
self.view.addSubview(datePicker)
self.datePicker = datePicker
}
@objc private func datePickerUpdated() {
self.item?.updated?(Int32(self.datePicker.date.timeIntervalSince1970))
guard let datePicker = self.datePicker else {
return
}
self.item?.updated?(Int32(datePicker.date.timeIntervalSince1970))
}
public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
@ -162,8 +168,8 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
strongSelf.datePicker.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
strongSelf.datePicker.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
strongSelf.datePicker?.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
strongSelf.datePicker?.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
switch item.style {
case .plain:

View File

@ -10,6 +10,7 @@ import ItemListUI
public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let invites: [ExportedInvitation]?
let share: Bool
public let sectionId: ItemListSectionId
let style: ItemListStyle
let tapAction: ((ExportedInvitation) -> Void)?
@ -19,6 +20,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
public init(
presentationData: ItemListPresentationData,
invites: [ExportedInvitation]?,
share: Bool,
sectionId: ItemListSectionId,
style: ItemListStyle,
tapAction: ((ExportedInvitation) -> Void)?,
@ -27,6 +29,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
) {
self.presentationData = presentationData
self.invites = invites
self.share = share
self.sectionId = sectionId
self.style = style
self.tapAction = tapAction
@ -127,15 +130,21 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
let topInset: CGFloat
if case .plain = item.style, case .otherSection = neighbors.top {
topInset = 16.0
} else {
topInset = 4.0
}
var height: CGFloat
let count = item.invites?.count ?? 0
if count > 0 {
if count % 2 == 0 {
height = 4.0 + 122.0 + 6.0
height = topInset + 122.0 + 6.0
} else {
height = 4.0 + 102.0 + 6.0
height = topInset + 102.0 + 6.0
}
} else {
height = 0.001
@ -143,9 +152,9 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
insets = itemListNeighborsPlainInsets(neighbors)
itemBackgroundColor = item.presentationData.theme.list.blocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.blocksBackgroundColor
insets = UIEdgeInsets()
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
@ -153,7 +162,7 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
}
if case .sameSection(false) = neighbors.bottom {
} else {
height += 6.0
height += 10.0
}
contentSize = CGSize(width: params.width, height: height)
@ -167,8 +176,8 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
let gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], presentationData: item.presentationData, transition: .immediate)
strongSelf.gridNode.frame = CGRect(origin: CGPoint(), size: gridSize)
let gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], share: item.share, presentationData: item.presentationData, transition: .immediate)
strongSelf.gridNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset - 4.0), size: gridSize)
strongSelf.gridNode.action = { invite in
item.tapAction?(invite)
}
@ -178,18 +187,19 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
if strongSelf.bottomStripeNode.supernode != nil {
strongSelf.bottomStripeNode.removeFromSupernode()
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {

View File

@ -30,9 +30,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let invite: ExportedInvitation?
let peers: [Peer]
let displayButton: Bool
let displayImporters: Bool
let buttonColor: UIColor?
public let sectionId: ItemListSectionId
let style: ItemListStyle
let copyAction: (() -> Void)?
let shareAction: (() -> Void)?
let contextAction: ((ASDisplayNode) -> Void)?
let viewAction: (() -> Void)?
@ -43,9 +46,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
presentationData: ItemListPresentationData,
invite: ExportedInvitation?,
peers: [Peer],
displayButton: Bool,
displayImporters: Bool,
buttonColor: UIColor?,
sectionId: ItemListSectionId,
style: ItemListStyle,
copyAction: (() -> Void)?,
shareAction: (() -> Void)?,
contextAction: ((ASDisplayNode) -> Void)?,
viewAction: (() -> Void)?,
@ -55,9 +61,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.presentationData = presentationData
self.invite = invite
self.peers = peers
self.displayButton = displayButton
self.displayImporters = displayImporters
self.buttonColor = buttonColor
self.sectionId = sectionId
self.style = style
self.copyAction = copyAction
self.shareAction = shareAction
self.contextAction = contextAction
self.viewAction = viewAction
@ -108,6 +117,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private let fieldNode: ASImageNode
private let addressNode: TextNode
private let fieldButtonNode: HighlightTrackingButtonNode
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let addressButtonNode: HighlightTrackingButtonNode
@ -151,12 +161,15 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.addressNode = TextNode()
self.addressNode.isUserInteractionEnabled = false
self.fieldButtonNode = HighlightTrackingButtonNode()
self.addressButtonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.addressButtonIconNode = ASImageNode()
self.addressButtonIconNode.contentMode = .center
self.addressButtonIconNode.displaysAsynchronously = false
self.addressButtonIconNode.displayWithoutProcessing = true
@ -171,6 +184,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.addSubnode(self.fieldNode)
self.addSubnode(self.addressNode)
self.addSubnode(self.fieldButtonNode)
self.addSubnode(self.avatarsNode)
self.addSubnode(self.invitedPeersNode)
self.addSubnode(self.avatarsButtonNode)
@ -183,6 +197,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.addSubnode(self.activateArea)
self.fieldButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.addressNode.layer.removeAnimation(forKey: "opacity")
strongSelf.addressNode.alpha = 0.4
} else {
strongSelf.addressNode.alpha = 1.0
strongSelf.addressNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.fieldButtonNode.addTarget(self, action: #selector(self.fieldButtonPressed), forControlEvents: .touchUpInside)
self.addressButtonNode.addTarget(self, action: #selector(self.addressButtonPressed), forControlEvents: .touchUpInside)
self.addressButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
@ -218,6 +245,12 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.avatarsButtonNode.addTarget(self, action: #selector(self.avatarsButtonPressed), forControlEvents: .touchUpInside)
}
@objc private func fieldButtonPressed() {
if let item = self.item {
item.copyAction?()
}
}
@objc private func addressButtonPressed() {
if let item = self.item {
item.contextAction?(self.extractedContainerNode)
@ -287,7 +320,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
switch item.style {
case .plain:
height -= 57.0
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = .clear
insets = UIEdgeInsets()
@ -296,6 +328,14 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors)
}
if !item.displayImporters {
height -= 57.0
}
if !item.displayButton {
height -= 63.0
}
contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -380,10 +420,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight))
strongSelf.fieldNode.frame = fieldFrame
strongSelf.fieldButtonNode.frame = fieldFrame
strongSelf.addressNode.frame = CGRect(origin: CGPoint(x: fieldFrame.minX + floorToScreenPixels((fieldFrame.width - addressLayout.size.width) / 2.0), y: fieldFrame.minY + floorToScreenPixels((fieldFrame.height - addressLayout.size.height) / 2.0) + 1.0), size: addressLayout.size)
strongSelf.addressButtonNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - 38.0, y: verticalInset + 14.0), size: CGSize(width: 24.0, height: 24.0))
strongSelf.addressButtonNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - 38.0 - 14.0, y: verticalInset), size: CGSize(width: 52.0, height: 52.0))
strongSelf.extractedContainerNode.frame = strongSelf.addressButtonNode.bounds
strongSelf.extractedContainerNode.contentRect = strongSelf.addressButtonNode.bounds
strongSelf.addressButtonIconNode.frame = strongSelf.addressButtonNode.bounds
@ -432,6 +473,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
strongSelf.avatarsButtonNode.frame = CGRect(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0, width: totalWidth, height: 32.0)
strongSelf.avatarsButtonNode.isUserInteractionEnabled = !item.peers.isEmpty
strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters
strongSelf.invitedPeersNode.isHidden = !item.displayImporters
}
})
}

View File

@ -18,6 +18,7 @@ public enum ItemListDisclosureStyle {
public enum ItemListDisclosureLabelStyle {
case text
case detailText
case coloredText(UIColor)
case multilineDetailText
case badge(UIColor)
case color(UIColor)
@ -277,6 +278,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = detailFont
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
case let .coloredText(color):
labelBadgeColor = color
labelFont = titleFont
default:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont

View File

@ -46,6 +46,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let clearType: ItemListSingleLineInputClearType
let maxLength: Int
let enabled: Bool
let selectAllOnFocus: Bool
public let sectionId: ItemListSectionId
let action: () -> Void
let textUpdated: (String) -> Void
@ -55,7 +56,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let cleared: (() -> Void)?
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
self.presentationData = presentationData
self.title = title
self.text = text
@ -67,6 +68,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
self.clearType = clearType
self.maxLength = maxLength
self.enabled = enabled
self.selectAllOnFocus = selectAllOnFocus
self.tag = tag
self.sectionId = sectionId
self.textUpdated = textUpdated
@ -494,6 +496,13 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
self.item?.updatedFocus?(true)
if self.item?.selectAllOnFocus == true {
DispatchQueue.main.async {
let startPosition = self.textNode.textField.beginningOfDocument
let endPosition = self.textNode.textField.endOfDocument
self.textNode.textField.selectedTextRange = self.textNode.textField.textRange(from: startPosition, to: endPosition)
}
}
self.updateClearButtonVisibility()
}

View File

@ -368,17 +368,17 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelAdminControllerArguments
switch self {
case let .info(theme, strings, dateTimeFormat, peer, presence):
case let .info(_, _, dateTimeFormat, peer, presence):
return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in
}, avatarTapped: {
})
case let .rankTitle(theme, text, count, limit):
case let .rankTitle(_, text, count, limit):
var accessoryText: ItemListSectionHeaderAccessoryText?
if let count = count {
accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic)
}
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section)
case let .rank(theme, strings, placeholder, text, enabled):
case let .rank(_, _, placeholder, text, enabled):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in
arguments.updateRank(text, updatedText)
}, shouldUpdateText: { text in
@ -392,23 +392,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
}, action: {
arguments.dismissInput()
})
case let .rankInfo(theme, text):
case let .rankInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .rightsTitle(theme, text):
case let .rightsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .rightItem(theme, _, text, right, flags, value, enabled):
case let .rightItem(_, _, text, right, flags, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggleRight(right, flags)
}, activatedWhileDisabled: {
arguments.toggleRightWhileDisabled(right, flags)
})
case let .addAdminsInfo(theme, text):
case let .addAdminsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .transfer(theme, text):
case let .transfer(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.transferOwnership()
}, tag: nil)
case let .dismiss(theme, text):
case let .dismiss(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.dismissAdmin()
}, tag: nil)
@ -1004,12 +1004,12 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
var currentRank: String?
var currentFlags: TelegramChatAdminRightsFlags?
switch initialParticipant {
case let .creator(creator):
currentRank = creator.rank
currentFlags = maskRightsFlags
case let .member(member):
case let .creator(_, adminInfo, rank):
currentRank = rank
currentFlags = adminInfo?.rights.flags ?? maskRightsFlags.subtracting(.canBeAnonymous)
case let .member(_, _, adminInfo, _, rank):
if updateFlags == nil {
if member.adminInfo?.rights == nil {
if adminInfo?.rights == nil {
if channel.flags.contains(.isCreator) {
updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous])
} else if let adminRights = channel.adminRights {
@ -1019,8 +1019,8 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
}
}
}
currentRank = member.rank
currentFlags = member.adminInfo?.rights.flags
currentRank = rank
currentFlags = adminInfo?.rights.flags
}
let effectiveRank = updateRank ?? currentRank

View File

@ -182,17 +182,17 @@ private enum ChannelMembersEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelMembersControllerArguments
switch self {
case let .addMember(theme, text):
case let .addMember(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.addMember()
})
case let .inviteLink(theme, text):
case let .inviteLink(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.inviteViaLink()
})
case let .addMemberInfo(theme, text):
case let .addMemberInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled):
case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled):
let text: ItemListPeerItemText
if let user = participant.peer as? TelegramUser, let _ = user.botInfo {
text = .text(strings.Bot_GenericBotStatus, .secondary)
@ -342,6 +342,8 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
var pushControllerImpl: ((ViewController) -> Void)?
var dismissInputImpl: (() -> Void)?
var getControllerImpl: (() -> ViewController?)?
let actionsDisposable = DisposableSet()
let addMembersDisposable = MetaDisposable()
@ -462,7 +464,10 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
pushControllerImpl?(controller)
}
}, inviteViaLink: {
pushControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId))
if let controller = getControllerImpl?() {
dismissInputImpl?()
presentControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId, parentNavigationController: controller.navigationController as? NavigationController), nil)
}
})
let peerView = context.account.viewTracker.peerView(peerId)
@ -551,6 +556,9 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
getControllerImpl = { [weak controller] in
return controller
}
controller.visibleBottomContentOffsetChanged = { offset in
if let loadMoreControl = loadMoreControl, case let .known(value) = offset, value < 40.0 {
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl)

View File

@ -30,11 +30,12 @@ private final class ChannelVisibilityControllerArguments {
let displayPrivateLinkMenu: (String) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let revokePeerId: (PeerId) -> Void
let shareLink: () -> Void
let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void
let linkContextAction: (ASDisplayNode) -> Void
let manageInviteLinks: () -> Void
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, shareLink: @escaping () -> Void, linkContextAction: @escaping (ASDisplayNode) -> Void, manageInviteLinks: @escaping () -> Void) {
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode) -> Void, manageInviteLinks: @escaping () -> Void) {
self.context = context
self.updateCurrentType = updateCurrentType
self.updatePublicLinkText = updatePublicLinkText
@ -42,6 +43,7 @@ private final class ChannelVisibilityControllerArguments {
self.displayPrivateLinkMenu = displayPrivateLinkMenu
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.revokePeerId = revokePeerId
self.copyLink = copyLink
self.shareLink = shareLink
self.linkContextAction = linkContextAction
self.manageInviteLinks = manageInviteLinks
@ -77,7 +79,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case publicLinkAvailability(PresentationTheme, String, Bool)
case editablePublicLink(PresentationTheme, PresentationStrings, String, String)
case privateLinkHeader(PresentationTheme, String)
case privateLink(PresentationTheme, ExportedInvitation?)
case privateLink(PresentationTheme, ExportedInvitation?, Bool)
case privateLinkInfo(PresentationTheme, String)
case privateLinkManage(PresentationTheme, String)
case privateLinkManageInfo(PresentationTheme, String)
@ -123,18 +125,18 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return 8
case .privateLinkInfo:
return 9
case .privateLinkManage:
return 10
case .privateLinkManageInfo:
return 11
case .publicLinkStatus:
return 12
return 10
case .publicLinkInfo:
return 13
return 11
case .existingLinksInfo:
return 14
return 12
case let .existingLinkPeerItem(index, _, _, _, _, _, _, _):
return 15 + index
return 13 + index
case .privateLinkManage:
return 1000
case .privateLinkManageInfo:
return 1001
}
}
@ -182,8 +184,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
} else {
return false
}
case let .privateLink(lhsTheme, lhsInvite):
if case let .privateLink(rhsTheme, rhsInvite) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite {
case let .privateLink(lhsTheme, lhsInvite, lhsDisplayImporters):
if case let .privateLink(rhsTheme, rhsInvite, rhsDisplayImporters) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, lhsDisplayImporters == rhsDisplayImporters {
return true
} else {
return false
@ -290,9 +292,15 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section)
case let .privateLinkHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .privateLink(_, invite):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
arguments.shareLink()
case let .privateLink(_, invite, displayImporters):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
if let invite = invite {
arguments.copyLink(invite)
}
}, shareAction: {
if let invite = invite {
arguments.shareLink(invite)
}
}, contextAction: { node in
arguments.linkContextAction(node)
}, viewAction: {
@ -586,15 +594,22 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_PublicLink_Info))
} else {
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp))
}
}
} else {
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePublicLinkHelp))
}
switch mode {
case .initialSetup:
break
case .generic, .privateLink:
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
}
}
case .privateChannel:
let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
entries.append(.privateLink(presentationData.theme, invite))
entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup))
if isGroup {
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
} else {
@ -613,7 +628,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
case .privateLink:
let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
entries.append(.privateLink(presentationData.theme, invite))
entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup))
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_Help))
switch mode {
case .initialSetup:
@ -712,7 +727,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
case .privateChannel:
let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
entries.append(.privateLink(presentationData.theme, invite))
entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup))
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
switch mode {
case .initialSetup:
@ -835,14 +850,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
let revokeLinkDisposable = MetaDisposable()
actionsDisposable.add(revokeLinkDisposable)
actionsDisposable.add((context.account.viewTracker.peerView(peerId) |> filter { $0.cachedData != nil } |> take(1) |> mapToSignal { view -> Signal<String?, NoError> in
return ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId)
|> mapToSignal { _ -> Signal<String?, NoError> in
return .complete()
}
}).start())
let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in
updateState { state in
return state.withUpdatedSelectedType(type)
@ -898,22 +906,13 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}
})
}))
}, shareLink: {
let _ = (context.account.postbox.transaction { transaction -> String? in
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation?.link
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation?.link
}
}
return nil
} |> deliverOnMainQueue).start(next: { link in
if let link = link {
let shareController = ShareController(context: context, subject: .url(link))
presentControllerImpl?(shareController, nil)
}
})
}, copyLink: { invite in
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
}, shareLink: { invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
}, linkContextAction: { node in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
return
@ -991,7 +990,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}
}
if revoke {
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
updateState {
$0.withUpdatedRevokingPrivateLink(false)
}
@ -1066,8 +1065,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
updateState { state in
return state.withUpdatedUpdatingAddressName(false)
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}, completed: {
updateState { state in
return state.withUpdatedUpdatingAddressName(false)

View File

@ -3,20 +3,24 @@ import Postbox
public final class CachedStickerQueryResult: PostboxCoding {
public let items: [TelegramMediaFile]
public let hash: Int32
public let timestamp: Int32
public init(items: [TelegramMediaFile], hash: Int32) {
public init(items: [TelegramMediaFile], hash: Int32, timestamp: Int32) {
self.items = items
self.hash = hash
self.timestamp = timestamp
}
public init(decoder: PostboxDecoder) {
self.items = decoder.decodeObjectArrayForKey("it").map { $0 as! TelegramMediaFile }
self.hash = decoder.decodeInt32ForKey("h", orElse: 0)
self.timestamp = decoder.decodeInt32ForKey("t", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.items, forKey: "it")
encoder.encodeInt32(self.hash, forKey: "h")
encoder.encodeInt32(self.timestamp, forKey: "t")
}
public static func cacheKey(_ query: String) -> ValueBoxKey {

View File

@ -66,4 +66,8 @@ public struct ExportedInvitation: PostboxCoding, Equatable {
public static func ==(lhs: ExportedInvitation, rhs: ExportedInvitation) -> Bool {
return lhs.link == rhs.link && lhs.isPermanent == rhs.isPermanent && lhs.isRevoked == rhs.isRevoked && lhs.adminId == rhs.adminId && lhs.date == rhs.date && lhs.startDate == rhs.startDate && lhs.expireDate == rhs.expireDate && lhs.usageLimit == rhs.usageLimit && lhs.count == rhs.count
}
public func withUpdated(isRevoked: Bool) -> ExportedInvitation {
return ExportedInvitation(link: self.link, isPermanent: self.isPermanent, isRevoked: isRevoked, adminId: self.adminId, date: self.date, startDate: self.startDate, expireDate: self.expireDate, usageLimit: self.usageLimit, count: self.count)
}
}

View File

@ -74,6 +74,7 @@ public struct Namespaces {
public static let cachedContextResults: Int8 = 10
public static let proximityNotificationStoredState: Int8 = 11
public static let cachedPeerInvitationImporters: Int8 = 12
public static let cachedPeerExportedInvitations: Int8 = 13
}
public struct UnorderedItemList {

View File

@ -3889,15 +3889,16 @@ public extension Api {
})
}
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetDate: Int32?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
let buffer = Buffer()
buffer.appendInt32(1838984707)
buffer.appendInt32(1785900140)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {adminId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetDate", offsetDate), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
let reader = BufferReader(buffer)
var result: Api.messages.ExportedChatInvites?
if let signature = reader.readInt32() {

View File

@ -1060,25 +1060,7 @@ public final class VoiceChatController: ViewController {
if !strongSelf.didSetDataReady {
strongSelf.accountPeer = accountPeer
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
if let peer = peerViewMainPeer(view) {
if let channel = peer as? TelegramChannel {
let addressName = channel.addressName ?? ""
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
if addressName.isEmpty {
let _ = ensuredExistingPeerExportedInvitation(account: strongSelf.context.account, peerId: call.peerId).start()
}
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
let _ = ensuredExistingPeerExportedInvitation(account: strongSelf.context.account, peerId: call.peerId).start()
default:
break
}
}
}
strongSelf.didSetDataReady = true
strongSelf.controller?.dataReady.set(true)
}

View File

@ -170,6 +170,8 @@ private var declaredEncodables: Void = {
declareEncodable(ValidationMessageAttribute.self, f: { ValidationMessageAttribute(decoder: $0) })
declareEncodable(EmojiSearchQueryMessageAttribute.self, f: { EmojiSearchQueryMessageAttribute(decoder: $0) })
declareEncodable(CachedPeerInvitationImporters.self, f: { CachedPeerInvitationImporters(decoder: $0) })
declareEncodable(CachedPeerExportedInvitations.self, f: { CachedPeerExportedInvitations(decoder: $0) })
declareEncodable(ExportedInvitation.self, f: { ExportedInvitation(decoder: $0) })
return
}()

View File

@ -6,53 +6,45 @@ import MtProtoKit
import SyncCore
public func ensuredExistingPeerExportedInvitation(account: Account, peerId: PeerId, revokeExisted: Bool = false) -> Signal<ExportedInvitation?, NoError> {
public func revokePersistentPeerExportedInvitation(account: Account, peerId: PeerId) -> Signal<ExportedInvitation?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
let flags: Int32 = (1 << 2)
if let _ = peer as? TelegramChannel {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, cachedData.exportedInvitation != nil && !revokeExisted {
return .single(cachedData.exportedInvitation)
} else {
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|> retryRequest
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedChannelData {
return current.withUpdatedExportedInvitation(invitation)
} else {
return CachedChannelData().withUpdatedExportedInvitation(invitation)
}
})
return invitation
} else {
return nil
}
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|> retryRequest
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedChannelData {
return current.withUpdatedExportedInvitation(invitation)
} else {
return CachedChannelData().withUpdatedExportedInvitation(invitation)
}
})
return invitation
} else {
return nil
}
}
}
} else if let _ = peer as? TelegramGroup {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData, cachedData.exportedInvitation != nil && !revokeExisted {
return .complete()
} else {
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|> retryRequest
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedGroupData {
return current.withUpdatedExportedInvitation(invitation)
} else {
return current
}
})
return invitation
} else {
return nil
}
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|> retryRequest
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedGroupData {
return current.withUpdatedExportedInvitation(invitation)
} else {
return current
}
})
return invitation
} else {
return nil
}
}
}
@ -65,8 +57,12 @@ public func ensuredExistingPeerExportedInvitation(account: Account, peerId: Peer
} |> switchToLatest
}
public func createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal<ExportedInvitation?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
public enum CreatePeerExportedInvitationError {
case generic
}
public func createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal<ExportedInvitation?, CreatePeerExportedInvitationError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, CreatePeerExportedInvitationError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
if let _ = expireDate {
@ -76,7 +72,7 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
flags |= (1 << 1)
}
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit))
|> retryRequest
|> mapError { _ in return CreatePeerExportedInvitationError.generic }
|> map { result -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) {
return invitation
@ -87,59 +83,9 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
} else {
return .complete()
}
} |> switchToLatest
}
public struct ExportedInvitations : Equatable {
public let list: [ExportedInvitation]?
public let totalCount: Int32
}
public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, offsetLink: String? = nil) -> Signal<ExportedInvitations?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitations?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
if let _ = offsetLink {
flags |= (1 << 2)
}
if revoked {
flags |= (1 << 3)
}
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetLink: offsetLink, limit: 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<ExportedInvitations?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitations? in
if let result = result, case let .exportedChatInvites(count, apiInvites, users) = result {
var peers: [Peer] = []
var peersMap: [PeerId: Peer] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
peersMap[telegramUser.id] = telegramUser
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
var invites: [ExportedInvitation] = []
for apiInvite in apiInvites {
if let invite = ExportedInvitation(apiExportedInvite: apiInvite) {
invites.append(invite)
}
}
return ExportedInvitations(list: invites, totalCount: count)
} else {
return nil
}
}
}
} else {
return .single(nil)
}
} |> switchToLatest
}
|> castError(CreatePeerExportedInvitationError.self)
|> switchToLatest
}
public enum EditPeerExportedInvitationError {
@ -218,6 +164,59 @@ public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link:
|> switchToLatest
}
public struct ExportedInvitations : Equatable {
public let list: [ExportedInvitation]?
public let totalCount: Int32
}
public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, offsetLink: ExportedInvitation? = nil) -> Signal<ExportedInvitations?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitations?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
if let _ = offsetLink {
flags |= (1 << 2)
}
if revoked {
flags |= (1 << 3)
}
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetDate: offsetLink?.date, offsetLink: offsetLink?.link, limit: 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<ExportedInvitations?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitations? in
if let result = result, case let .exportedChatInvites(count, apiInvites, users) = result {
var peers: [Peer] = []
var peersMap: [PeerId: Peer] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
peersMap[telegramUser.id] = telegramUser
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
var invites: [ExportedInvitation] = []
for apiInvite in apiInvites {
if let invite = ExportedInvitation(apiExportedInvite: apiInvite) {
invites.append(invite)
}
}
return ExportedInvitations(list: invites, totalCount: count)
} else {
return nil
}
}
}
} else {
return .single(nil)
}
} |> switchToLatest
}
public enum DeletePeerExportedInvitationError {
case generic
}
@ -251,6 +250,320 @@ public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: Pe
|> switchToLatest
}
private let cachedPeerExportedInvitationsCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
public struct PeerExportedInvitationsState: Equatable {
public var invitations: [ExportedInvitation]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int32
}
final class CachedPeerExportedInvitations: PostboxCoding {
let invitations: [ExportedInvitation]
let canLoadMore: Bool
let count: Int32
public static func key(peerId: PeerId, revoked: Bool) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 4)
key.setInt64(0, value: peerId.toInt64())
key.setInt32(8, value: revoked ? 1 : 0)
return key
}
init(invitations: [ExportedInvitation], canLoadMore: Bool, count: Int32) {
self.invitations = invitations
self.canLoadMore = canLoadMore
self.count = count
}
public init(decoder: PostboxDecoder) {
self.invitations = decoder.decodeObjectArrayForKey("invitations")
self.canLoadMore = decoder.decodeBoolForKey("canLoadMore", orElse: false)
self.count = decoder.decodeInt32ForKey("count", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.invitations, forKey: "invitations")
encoder.encodeBool(self.canLoadMore, forKey: "canLoadMore")
encoder.encodeInt32(self.count, forKey: "count")
}
}
private final class PeerExportedInvitationsContextImpl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let revoked: Bool
private var forceUpdate: Bool
private let disposable = MetaDisposable()
private let updateDisposable = MetaDisposable()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var loadedFromCache: Bool = false
private var results: [ExportedInvitation] = []
private var count: Int32
private var populateCache: Bool = true
let state = Promise<PeerExportedInvitationsState>()
init(queue: Queue, account: Account, peerId: PeerId, revoked: Bool, forceUpdate: Bool) {
self.queue = queue
self.account = account
self.peerId = peerId
self.revoked = revoked
self.forceUpdate = forceUpdate
self.count = 0
self.isLoadingMore = true
self.disposable.set((account.postbox.transaction { transaction -> CachedPeerExportedInvitations? in
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerExportedInvitations, key: CachedPeerExportedInvitations.key(peerId: peerId, revoked: revoked))) as? CachedPeerExportedInvitations
}
|> deliverOn(self.queue)).start(next: { [weak self] cachedResult in
guard let strongSelf = self else {
return
}
strongSelf.isLoadingMore = false
if let cachedResult = cachedResult {
strongSelf.results = cachedResult.invitations
strongSelf.count = cachedResult.count
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = cachedResult.canLoadMore
strongSelf.loadedFromCache = true
}
strongSelf.loadMore()
}))
self.loadMore()
}
deinit {
self.disposable.dispose()
self.updateDisposable.dispose()
}
func reload() {
self.forceUpdate = true
self.loadMore()
}
func loadMore() {
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let account = self.account
let peerId = self.peerId
let revoked = self.revoked
var lastResult = self.results.last
if self.forceUpdate {
self.forceUpdate = false
lastResult = nil
}
if !self.forceUpdate && self.loadedFromCache {
self.populateCache = false
self.loadedFromCache = false
}
let populateCache = self.populateCache
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([ExportedInvitation], Int32), NoError> in
if let inputPeer = inputPeer {
let offsetLink = lastResult?.link
let offsetDate = lastResult?.date
var flags: Int32 = 0
if let _ = offsetLink {
flags |= (1 << 2)
}
if revoked {
flags |= (1 << 3)
}
let signal = account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetDate: offsetDate, offsetLink: offsetLink, limit: lastResult == nil ? 50 : 100))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([ExportedInvitation], Int32), NoError> in
return account.postbox.transaction { transaction -> ([ExportedInvitation], Int32) in
guard let result = result else {
return ([], 0)
}
switch result {
case let .exportedChatInvites(count, invites, users):
var peers: [Peer] = []
for apiUser in users {
peers.append(TelegramUser(user: apiUser))
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated in
return updated
})
let invitations: [ExportedInvitation] = invites.compactMap { ExportedInvitation(apiExportedInvite: $0) }
if populateCache {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerExportedInvitations, key: CachedPeerExportedInvitations.key(peerId: peerId, revoked: revoked)), entry: CachedPeerExportedInvitations(invitations: invitations, canLoadMore: count >= 50, count: count), collectionSpec: cachedPeerExportedInvitationsCollectionSpec)
}
return (invitations, count)
}
}
}
return signal
} else {
return .single(([], 0))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] invitations, updatedCount in
guard let strongSelf = self else {
return
}
if strongSelf.populateCache {
strongSelf.populateCache = false
strongSelf.results.removeAll()
}
var existingLinks = Set(strongSelf.results.map { $0.link })
for invitation in invitations {
if !existingLinks.contains(invitation.link) {
strongSelf.results.append(invitation)
existingLinks.insert(invitation.link)
}
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !invitations.isEmpty
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
} else {
strongSelf.count = Int32(strongSelf.results.count)
}
strongSelf.updateState()
if strongSelf.forceUpdate {
strongSelf.loadMore()
}
}))
self.updateState()
}
public func add(_ invite: ExportedInvitation) {
var results = self.results
results.removeAll(where: { $0.link == invite.link})
results.insert(invite, at: 0)
self.results = results
self.updateState()
self.updateCache()
}
public func update(_ invite: ExportedInvitation) {
var results = self.results
if let index = self.results.firstIndex(where: { $0.link == invite.link }) {
results[index] = invite
}
self.results = results
self.updateState()
self.updateCache()
}
public func remove(_ invite: ExportedInvitation) {
var results = self.results
results.removeAll(where: { $0.link == invite.link})
self.results = results
self.updateState()
self.updateCache()
}
public func clear() {
self.results = []
self.count = 0
self.updateState()
self.updateCache()
}
private func updateCache() {
guard self.hasLoadedOnce && !self.isLoadingMore else {
return
}
let peerId = self.peerId
let revoked = self.revoked
let invitations = Array(self.results.prefix(50))
let canLoadMore = self.canLoadMore
let count = self.count
self.updateDisposable.set(self.account.postbox.transaction({ transaction in
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerExportedInvitations, key: CachedPeerExportedInvitations.key(peerId: peerId, revoked: revoked)), entry: CachedPeerExportedInvitations(invitations: invitations, canLoadMore: canLoadMore, count: count), collectionSpec: cachedPeerExportedInvitationsCollectionSpec)
}).start())
}
private func updateState() {
self.state.set(.single(PeerExportedInvitationsState(invitations: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public final class PeerExportedInvitationsContext {
private let queue: Queue = Queue()
private let impl: QueueLocalObject<PeerExportedInvitationsContextImpl>
public var state: Signal<PeerExportedInvitationsState, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(account: Account, peerId: PeerId, revoked: Bool, forceUpdate: Bool) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PeerExportedInvitationsContextImpl(queue: queue, account: account, peerId: peerId, revoked: revoked, forceUpdate: forceUpdate)
})
}
public func reload() {
self.impl.with { impl in
impl.reload()
}
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
public func add(_ invite: ExportedInvitation) {
self.impl.with { impl in
impl.add(invite)
}
}
public func update(_ invite: ExportedInvitation) {
self.impl.with { impl in
impl.update(invite)
}
}
public func remove(_ invite: ExportedInvitation) {
self.impl.with { impl in
impl.remove(invite)
}
}
public func clear() {
self.impl.with { impl in
impl.clear()
}
}
}
private let cachedPeerInvitationImportersCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
public struct PeerInvitationImportersState: Equatable {
@ -262,7 +575,7 @@ public struct PeerInvitationImportersState: Equatable {
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int
public var count: Int32
}
final class CachedPeerInvitationImporters: PostboxCoding {
@ -330,8 +643,9 @@ private final class PeerInvitationImportersContextImpl {
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var loadedFromCache = false
private var results: [PeerInvitationImportersState.Importer] = []
private var count: Int
private var count: Int32
private var populateCache: Bool = true
let state = Promise<PeerInvitationImportersState>()
@ -342,7 +656,7 @@ private final class PeerInvitationImportersContextImpl {
self.peerId = peerId
self.link = invite.link
let count = invite.count.flatMap { Int($0) } ?? 0
let count = invite.count ?? 0
self.count = count
self.isLoadingMore = true
@ -371,6 +685,7 @@ private final class PeerInvitationImportersContextImpl {
strongSelf.results = cachedPeers
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = canLoadMore
strongSelf.loadedFromCache = true
}
strongSelf.loadMore()
}))
@ -390,12 +705,18 @@ private final class PeerInvitationImportersContextImpl {
let account = self.account
let peerId = self.peerId
let link = self.link
let lastResult = self.results.last
let populateCache = self.populateCache
var lastResult = self.results.last
if self.loadedFromCache {
self.loadedFromCache = false
lastResult = nil
}
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in
|> mapToSignal { inputPeer -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
if let inputPeer = inputPeer {
let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty
let offsetDate = lastResult?.date ?? 0
@ -404,8 +725,8 @@ private final class PeerInvitationImportersContextImpl {
|> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int) in
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int32) in
guard let result = result else {
return ([], 0)
}
@ -434,7 +755,7 @@ private final class PeerInvitationImportersContextImpl {
if populateCache {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerInvitationImporters, key: CachedPeerInvitationImporters.key(peerId: peerId, link: link)), entry: CachedPeerInvitationImporters(importers: resultImporters, count: count), collectionSpec: cachedPeerInvitationImportersCollectionSpec)
}
return (resultImporters, Int(count))
return (resultImporters, count)
}
}
}
@ -462,9 +783,9 @@ private final class PeerInvitationImportersContextImpl {
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !importers.isEmpty
if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, strongSelf.results.count)
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
} else {
strongSelf.count = strongSelf.results.count
strongSelf.count = Int32(strongSelf.results.count)
}
strongSelf.updateState()
}))

View File

@ -14,6 +14,14 @@ public struct TelegramPeerPhoto {
public let index: Int
public let totalCount: Int
public let messageId: MessageId?
public init(image: TelegramMediaImage, reference: TelegramMediaImageReference?, date: Int32, index: Int, totalCount: Int, messageId: MessageId?) {
self.image = image
self.reference = reference
self.date = date
self.index = index
self.totalCount = totalCount
self.messageId = messageId
}
}
public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> {

View File

@ -5,6 +5,26 @@ import SwiftSignalKit
import SyncCore
private struct SearchStickersConfiguration {
static var defaultValue: SearchStickersConfiguration {
return SearchStickersConfiguration(cacheTimeout: 86400)
}
public let cacheTimeout: Int32
fileprivate init(cacheTimeout: Int32) {
self.cacheTimeout = cacheTimeout
}
static func with(appConfiguration: AppConfiguration) -> SearchStickersConfiguration {
if let data = appConfiguration.data, let value = data["stickers_emoji_cache_time"] as? Int32 {
return SearchStickersConfiguration(cacheTimeout: value)
} else {
return .defaultValue
}
}
}
public final class FoundStickerItem: Equatable {
public let file: TelegramMediaFile
public let stringRepresentations: [String]
@ -140,7 +160,15 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
result.append(contentsOf: installedItems)
}
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query))) as? CachedStickerQueryResult
var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query))) as? CachedStickerQueryResult
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue
let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration)
if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout {
cached = nil
}
return (result, cached)
} |> mapToSignal { localItems, cached -> Signal<[FoundStickerItem], NoError> in
@ -199,7 +227,8 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
result.append(contentsOf: animatedItems)
result.append(contentsOf: items)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)), entry: CachedStickerQueryResult(items: files, hash: hash), collectionSpec: collectionSpec)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)), entry: CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime), collectionSpec: collectionSpec)
return result
case .stickersNotModified:

View File

@ -6,6 +6,7 @@ import SyncCore
public enum ServerProvidedSuggestion: String {
case autoarchivePopular = "AUTOARCHIVE_POPULAR"
case newcomerTicks = "NEWCOMER_TICKS"
}
public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProvidedSuggestion], NoError> {
@ -22,12 +23,7 @@ public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProv
return []
}
return list.compactMap { item -> ServerProvidedSuggestion? in
switch item {
case "AUTOARCHIVE_POPULAR":
return .autoarchivePopular
default:
return nil
}
return ServerProvidedSuggestion(rawValue: item)
}
}
|> distinctUntilChanged

View File

@ -205,10 +205,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
let peerStatusSettings = PeerStatusSettings(apiSettings: userFull.settings)
var hasScheduledMessages = false
if (userFull.flags & 1 << 12) != 0 {
hasScheduledMessages = true
}
let hasScheduledMessages = (userFull.flags & 1 << 12) != 0
return previous.withUpdatedAbout(userFull.about).withUpdatedBotInfo(botInfo).withUpdatedCommonGroupCount(userFull.commonChatsCount).withUpdatedIsBlocked(isBlocked).withUpdatedVoiceCallsAvailable(voiceCallsAvailable).withUpdatedVideoCallsAvailable(videoCallsAvailable).withUpdatedCallsPrivate(callsPrivate).withUpdatedCanPinMessages(canPinMessages).withUpdatedPeerStatusSettings(peerStatusSettings).withUpdatedPinnedMessageId(pinnedMessageId).withUpdatedHasScheduledMessages(hasScheduledMessages)
}
@ -257,7 +254,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
let photo: TelegramMediaImage? = chatFull.chatPhoto.flatMap(telegramMediaImageFromApiPhoto)
let exportedInvitation = chatFull.exportedInvite.flatMap(ExportedInvitation.init(apiExportedInvite:))
let exportedInvitation = chatFull.exportedInvite.flatMap { ExportedInvitation(apiExportedInvite: $0) }
let pinnedMessageId = chatFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) })
var peers: [Peer] = []

View File

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

View File

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

View File

@ -305,6 +305,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var applicationInForegroundDisposable: Disposable?
private var applicationInFocusDisposable: Disposable?
private let checksTooltipDisposable = MetaDisposable()
private var shouldDisplayChecksTooltip = false
private var checkedPeerChatServiceActions = false
private var willAppear = false
@ -3424,6 +3427,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.peekTimerDisposable.dispose()
self.hasActiveGroupCallDisposable?.dispose()
self.createVoiceChatDisposable.dispose()
self.checksTooltipDisposable.dispose()
}
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
@ -4761,7 +4765,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
let editingMessage = strongSelf.editingMessage
let text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText))
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
var entitiesAttribute: TextEntitiesMessageAttribute?
@ -6236,7 +6239,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.openScheduledMessages()
}
// strongSelf.displayChecksTooltip()
if strongSelf.shouldDisplayChecksTooltip {
strongSelf.displayChecksTooltip()
strongSelf.shouldDisplayChecksTooltip = false
strongSelf.checksTooltipDisposable.set(dismissServerProvidedSuggestion(account: strongSelf.context.account, suggestion: .newcomerTicks).start())
}
}
}))
@ -6482,7 +6489,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false
}
if !strongSelf.context.sharedContext.currentMediaInputSettings.with { $0.enableRaiseToSpeak } {
if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) {
return false
}
@ -6707,6 +6714,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
}
self.checksTooltipDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox)
|> deliverOnMainQueue).start(next: { [weak self] values in
guard let strongSelf = self else {
return
}
if !values.contains(.newcomerTicks) {
return
}
strongSelf.shouldDisplayChecksTooltip = true
}))
if self.scheduledActivateInput {
self.scheduledActivateInput = false
@ -7481,7 +7499,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|> deliverOnMainQueue).start(next: { [weak self, weak controller] result in
controller?.dismiss()
guard let strongSelf = self, case let .result(stats) = result, var categories = stats.media[peer.id] else {
guard let strongSelf = self, case let .result(stats) = result, let categories = stats.media[peer.id] else {
return
}
let presentationData = strongSelf.presentationData
@ -7682,16 +7700,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) {
if let message = messages.first, case let .message(desc) = message, let mediaReference = desc.mediaReference {
if let message = messages.first, case let .message(text, _, maybeMediaReference, _, _) = message, let mediaReference = maybeMediaReference {
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var state = state
if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty {
state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
}
if !desc.text.isEmpty {
if !text.isEmpty {
state = state.updatedInterfaceState { state in
if let editMessage = state.editMessage {
return state.withUpdatedEditMessage(editMessage.withUpdatedInputState(ChatTextInputState(inputText: NSAttributedString(string: desc.text))))
return state.withUpdatedEditMessage(editMessage.withUpdatedInputState(ChatTextInputState(inputText: NSAttributedString(string: text))))
}
return state
}
@ -9338,7 +9356,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
let complete = results.completed
var navigateIndex: MessageIndex?
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search {
@ -10113,8 +10130,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
case let .withBotStartPayload(botStart):
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(peerId), botStart: botStart))
default:
break
}
}
} else {
@ -10536,7 +10551,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) {
self.commitPurposefulAction()
self.presentVoiceMessageDiscardAlert(action: {
let _ = self.presentVoiceMessageDiscardAlert(action: {
if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback {
if url.hasSuffix(".m3u8") {
let navigationController = self.navigationController as? NavigationController

View File

@ -159,6 +159,7 @@ final class PeerInfoScreenData {
let members: PeerInfoMembersData?
let encryptionKeyFingerprint: SecretChatKeyFingerprint?
let globalSettings: TelegramGlobalSettings?
let invitations: PeerExportedInvitationsState?
init(
peer: Peer?,
@ -172,7 +173,8 @@ final class PeerInfoScreenData {
linkedDiscussionPeer: Peer?,
members: PeerInfoMembersData?,
encryptionKeyFingerprint: SecretChatKeyFingerprint?,
globalSettings: TelegramGlobalSettings?
globalSettings: TelegramGlobalSettings?,
invitations: PeerExportedInvitationsState?
) {
self.peer = peer
self.cachedData = cachedData
@ -186,6 +188,7 @@ final class PeerInfoScreenData {
self.members = members
self.encryptionKeyFingerprint = encryptionKeyFingerprint
self.globalSettings = globalSettings
self.invitations = invitations
}
}
@ -442,7 +445,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account
linkedDiscussionPeer: nil,
members: nil,
encryptionKeyFingerprint: nil,
globalSettings: globalSettings
globalSettings: globalSettings,
invitations: nil
)
}
}
@ -464,7 +468,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: nil,
members: nil,
encryptionKeyFingerprint: nil,
globalSettings: nil
globalSettings: nil,
invitations: nil
))
case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext?
@ -603,13 +608,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: nil,
members: nil,
encryptionKeyFingerprint: encryptionKeyFingerprint,
globalSettings: nil
globalSettings: nil,
invitations: nil
)
}
case .channel:
let status = context.account.viewTracker.peerView(peerId, updateData: false)
|> map { peerView -> PeerInfoStatusData? in
guard let channel = peerView.peers[peerId] as? TelegramChannel else {
guard let _ = peerView.peers[peerId] as? TelegramChannel else {
return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false)
}
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 {
@ -623,13 +629,19 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
var combinedKeys: [PostboxViewKey] = []
combinedKeys.append(globalNotificationsKey)
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId),
context.account.postbox.combinedView(keys: combinedKeys),
status
status,
invitationsContextPromise.get(),
invitationsStatePromise.get()
)
|> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in
|> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations -> PeerInfoScreenData in
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
@ -642,6 +654,18 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
discussionPeer = peer
}
if currentInvitationsContext == nil {
var canManageInvitations = false
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || ((channel.adminRights != nil && channel.hasPermission(.pinMessages)) && cachedData.flags.contains(.canChangeUsername)) {
canManageInvitations = true
}
if canManageInvitations {
let invitationsContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: true)
invitationsContextPromise.set(.single(invitationsContext))
invitationsStatePromise.set(invitationsContext.state |> map(Optional.init))
}
}
return PeerInfoScreenData(
peer: peerView.peers[peerId],
cachedData: peerView.cachedData,
@ -654,7 +678,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: discussionPeer,
members: nil,
encryptionKeyFingerprint: nil,
globalSettings: nil
globalSettings: nil,
invitations: invitations
)
}
case let .group(groupId):
@ -751,14 +776,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
var combinedKeys: [PostboxViewKey] = []
combinedKeys.append(globalNotificationsKey)
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
return combineLatest(queue: .mainQueue(),
context.account.viewTracker.peerView(groupId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: groupId),
context.account.postbox.combinedView(keys: combinedKeys),
status,
membersData
membersData,
invitationsContextPromise.get(),
invitationsStatePromise.get()
)
|> map { peerView, availablePanes, combinedView, status, membersData -> PeerInfoScreenData in
|> map { peerView, availablePanes, combinedView, status, membersData, currentInvitationsContext, invitations -> PeerInfoScreenData in
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
@ -780,6 +811,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
}
}
if currentInvitationsContext == nil {
var canManageInvitations = false
if let group = peerViewMainPeer(peerView) as? TelegramGroup, case .creator = group.role {
canManageInvitations = true
} else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || ((channel.adminRights != nil && channel.hasPermission(.pinMessages)) && cachedData.flags.contains(.canChangeUsername)) {
canManageInvitations = true
}
if canManageInvitations {
let invitationsContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: true)
invitationsContextPromise.set(.single(invitationsContext))
invitationsStatePromise.set(invitationsContext.state |> map(Optional.init))
}
}
return PeerInfoScreenData(
peer: peerView.peers[groupId],
cachedData: peerView.cachedData,
@ -792,7 +837,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: discussionPeer,
members: membersData,
encryptionKeyFingerprint: nil,
globalSettings: nil
globalSettings: nil,
invitations: invitations
)
}
}

View File

@ -1185,6 +1185,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
discussionGroupTitle = "..."
}
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemDiscussionGroup, label: .text(discussionGroupTitle), text: presentationData.strings.Channel_DiscussionGroup, action: {
interaction.editingOpenDiscussionGroupSetup()
}))
@ -1255,12 +1266,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
interaction.editingOpenPublicLinkSetup()
}))
if !isPublic {
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
if let linkedDiscussionPeer = data.linkedDiscussionPeer {
let peerTitle: String
if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty {
@ -1318,9 +1334,10 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
}
} else if let group = data.peer as? TelegramGroup {
let ItemUsername = 1
let ItemPreHistory = 2
let ItemPermissions = 3
let ItemAdmins = 4
let ItemInviteLinks = 2
let ItemPreHistory = 3
let ItemPermissions = 4
let ItemAdmins = 5
if case .creator = group.role {
if let cachedData = data.cachedData as? CachedGroupData {
@ -1330,6 +1347,18 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
}))
}
}
let invitesText: String
if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)"
} else {
invitesText = ""
}
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup()
}))
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistory, action: {
interaction.editingOpenPreHistorySetup()
}))
@ -4540,15 +4569,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
guard let strongSelf = self else {
return
}
let mode: ChannelVisibilityControllerMode
if groupPeer.addressName != nil {
let visibilityController = channelVisibilityController(context: strongSelf.context, peerId: groupPeer.id, mode: .generic, upgradedToSupergroup: { _, f in f() }, onDismissRemoveController: contactsController)
//visibilityController.navigationPresentation = .modal
contactsController?.push(visibilityController)
} else {
contactsController?.push(InviteLinkInviteController(context: context, peerId: groupPeer.id))
}
strongSelf.view.endEditing(true)
contactsController?.present(InviteLinkInviteController(context: context, peerId: groupPeer.id, parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
}
strongSelf.controller?.push(contactsController)

View File

@ -70,9 +70,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.addSubnode(self.imageNode)
self.addSubnode(self.actionArea)
self.messageDisposable.set((context.account.postbox.messageAtId(messageId)
|> deliverOnMainQueue).start(next: { [weak self] message in
self.messageDisposable.set((context.account.postbox.messageView(messageId)
|> deliverOnMainQueue).start(next: { [weak self] messageView in
if let strongSelf = self {
let message = messageView.message
var authorName = ""
var text = ""
if let author = message?.effectiveAuthor {

View File

@ -91,8 +91,8 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
}
let resolvedWallpaper: Signal<TelegramWallpaper?, NoError>
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
resolvedWallpaper = cachedWallpaper(account: account, slug: file.slug, settings: file.settings)
if case let .file(id, _, _, _, _, _, slug, _, settings) = presentationTheme.chat.defaultWallpaper, id == 0 {
resolvedWallpaper = cachedWallpaper(account: account, slug: slug, settings: settings)
|> map { wallpaper in
return wallpaper?.wallpaper
}
@ -102,15 +102,15 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
return resolvedWallpaper
|> mapToSignal { wallpaper -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in
if let wallpaper = wallpaper, case let .file(file) = wallpaper {
if let wallpaper = wallpaper, case let .file(_, _, _, _, _, _, slug, file, _) = wallpaper {
var convertedRepresentations: [ImageRepresentationWithReference] = []
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(slug), resource: file.resource)))
return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
|> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in
guard complete, let fullSizeData = fullSizeData else {
return .complete()
}
accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true)
accountManager.mediaBox.storeResourceData(file.resource.id, data: fullSizeData, synchronous: true)
return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme))
}
} else {