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

View File

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

View File

@ -158,7 +158,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: { return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
interaction.activateSearch() interaction.activateSearch()
}) })
case let .sort(theme, strings, sortOrder): case let .sort(_, strings, sortOrder):
var text = strings.Contacts_SortedByName var text = strings.Contacts_SortedByName
if case .presence = sortOrder { if case .presence = sortOrder {
text = strings.Contacts_SortedByPresence 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: { return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
interaction.openSortMenu() 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 : { return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
interaction.suppressWarning() interaction.suppressWarning()
}) })
case let .permissionEnable(theme, text): case let .permissionEnable(_, text):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: { return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: {
interaction.authorize() 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) 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 var status: ContactsPeerItemStatus
let itemPeer: ContactsPeerItemPeer let itemPeer: ContactsPeerItemPeer
var isContextActionEnabled = false var isContextActionEnabled = false
@ -928,9 +928,9 @@ public final class ContactListNode: ASDisplayNode {
|> mapToSignal { presentation in |> mapToSignal { presentation in
var generateSections = false var generateSections = false
var includeChatList = false var includeChatList = false
if case let .natural(natural) = presentation { if case let .natural(_, includeChatListValue) = presentation {
generateSections = true generateSections = true
includeChatList = natural.includeChatList includeChatList = includeChatListValue
} }
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation { 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.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent()) self.centralItemFooterContentNode.set(centralItemNode.footerContent())
if let (media, _) = mediaForMessage(message: message) { if let _ = mediaForMessage(message: message) {
centralItemNode.activateAsInitial() centralItemNode.activateAsInitial()
} }
} }

View File

@ -50,7 +50,7 @@ func isValidNumberOfUsers(_ number: String) -> Bool {
private enum InviteLinksEditEntry: ItemListNodeEntry { private enum InviteLinksEditEntry: ItemListNodeEntry {
case timeHeader(PresentationTheme, String) case timeHeader(PresentationTheme, String)
case timePicker(PresentationTheme, InviteLinkTimeLimit) case timePicker(PresentationTheme, InviteLinkTimeLimit)
case timeExpiryDate(PresentationTheme, Int32?) case timeExpiryDate(PresentationTheme, Int32?, Bool)
case timeCustomPicker(PresentationTheme, Int32?) case timeCustomPicker(PresentationTheme, Int32?)
case timeInfo(PresentationTheme, String) case timeInfo(PresentationTheme, String)
@ -111,8 +111,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .timeExpiryDate(lhsTheme, lhsDate): case let .timeExpiryDate(lhsTheme, lhsDate, lhsActive):
if case let .timeExpiryDate(rhsTheme, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate { if case let .timeExpiryDate(rhsTheme, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate, lhsActive == rhsActive {
return true return true
} else { } else {
return false return false
@ -182,14 +182,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState return updatedState
}) })
}) })
case let .timeExpiryDate(_, value): case let .timeExpiryDate(theme, value, active):
let text: String let text: String
if let value = value { if let value = value {
text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ".")) text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."))
} else { } else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever 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.dismissInput()
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
@ -223,7 +223,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
}) })
case let .usageCustomPicker(theme, value, focused): case let .usageCustomPicker(theme, value, focused):
let text = value.flatMap { String($0) } ?? (focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited) 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 { guard !updatedText.isEmpty else {
return return
} }
@ -276,7 +276,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
} else if let value = state.time.value { } else if let value = state.time.value {
time = currentTime + value time = currentTime + value
} }
entries.append(.timeExpiryDate(presentationData.theme, time)) entries.append(.timeExpiryDate(presentationData.theme, time, state.pickingTimeLimit))
if state.pickingTimeLimit { if state.pickingTimeLimit {
entries.append(.timeCustomPicker(presentationData.theme, time ?? currentTime)) entries.append(.timeCustomPicker(presentationData.theme, time ?? currentTime))
} }
@ -300,9 +300,10 @@ private struct InviteLinkEditControllerState: Equatable {
var time: InviteLinkTimeLimit var time: InviteLinkTimeLimit
var pickingTimeLimit = false var pickingTimeLimit = false
var pickingUsageLimit = 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)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet() 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) initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false)
} else { } 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) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -359,8 +360,17 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
dismissAction() dismissAction()
dismissImpl?() dismissImpl?()
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: { let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link)
completion?() |> 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) presentControllerImpl?(controller, nil)
}) })
let previousState = Atomic<InviteLinkEditControllerState?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
@ -376,7 +387,13 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
dismissImpl?() 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? let expireDate: Int32?
if case let .custom(value) = state.time { if case let .custom(value) = state.time {
expireDate = value expireDate = value
@ -390,21 +407,43 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
let usageLimit = state.usage.value let usageLimit = state.usage.value
if invite == nil { if invite == nil {
let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit) let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|> deliverOnMainQueue).start(next: { result in |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
completion?() |> deliverOnMainQueue).start(next: { invite in
completion?(invite)
dismissImpl?() 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 { } else if let invite = invite {
let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit) let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|> deliverOnMainQueue).start(next: { result in |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
completion?() |> deliverOnMainQueue).start(next: { invite in
completion?(invite)
dismissImpl?() 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 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)) return (controllerState, (listState, arguments))
} }

View File

@ -45,25 +45,39 @@ private struct InviteLinkInviteTransaction {
} }
private enum InviteLinkInviteEntryId: Hashable { private enum InviteLinkInviteEntryId: Hashable {
case header
case mainLink case mainLink
case links(Int32) case links(Int32)
case manage
} }
private enum InviteLinkInviteEntry: Comparable, Identifiable { private enum InviteLinkInviteEntry: Comparable, Identifiable {
case header(PresentationTheme, String, String)
case mainLink(PresentationTheme, ExportedInvitation) case mainLink(PresentationTheme, ExportedInvitation)
case links(Int32, PresentationTheme, [ExportedInvitation]) case links(Int32, PresentationTheme, [ExportedInvitation])
case manage(PresentationTheme, String, Bool)
var stableId: InviteLinkInviteEntryId { var stableId: InviteLinkInviteEntryId {
switch self { switch self {
case .header:
return .header
case .mainLink: case .mainLink:
return .mainLink return .mainLink
case let .links(index, _, _): case let .links(index, _, _):
return .links(index) return .links(index)
case .manage:
return .manage
} }
} }
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool { static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs { 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): case let .mainLink(lhsTheme, lhsInvitation):
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation { if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
return true return true
@ -76,43 +90,73 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
} else { } else {
return false 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 { static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs { switch lhs {
case .header:
switch rhs {
case .header:
return false
case .mainLink, .links, .manage:
return true
}
case .mainLink: case .mainLink:
switch rhs { switch rhs {
case .mainLink: case .header, .mainLink:
return false return false
case .links: case .links, .manage:
return true return true
} }
case let .links(lhsIndex, _, _): case let .links(lhsIndex, _, _):
switch rhs { switch rhs {
case .mainLink: case .header, .mainLink:
return false return false
case let .links(rhsIndex, _, _): case let .links(rhsIndex, _, _):
return lhsIndex < 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 { func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
switch self { switch self {
case let .header(theme, title, text):
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
case let .mainLink(_, invite): 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) interaction.shareLink(invite)
}, contextAction: { node in }, contextAction: { node in
interaction.mainLinkContextAction(invite, node, nil) interaction.mainLinkContextAction(invite, node, nil)
}, viewAction: { }, viewAction: {
}) })
case let .links(_, _, invites): 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) interaction.copyLink(invite)
}, contextAction: { invite, _ in }, contextAction: { invite, _ in
interaction.shareLink(invite) 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 context: AccountContext
private let peerId: PeerId private let peerId: PeerId
private weak var parentNavigationController: NavigationController?
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId) { public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) {
fatalError()
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.parentNavigationController = parentNavigationController
super.init(navigationBarPresentationData: nil) super.init(navigationBarPresentationData: nil)
@ -196,7 +241,7 @@ public final class InviteLinkInviteController: ViewController {
self.controllerNode.animateOut(completion: { [weak self] in self.controllerNode.animateOut(completion: { [weak self] in
completion?() 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 context: AccountContext
private let peerId: PeerId private let peerId: PeerId
private let invitesContext: PeerExportedInvitationsContext
private var interaction: InviteLinkInviteInteraction? private var interaction: InviteLinkInviteInteraction?
@ -245,6 +291,8 @@ public final class InviteLinkInviteController: ViewController {
self.presentationDataPromise = Promise(self.presentationData) self.presentationDataPromise = Promise(self.presentationData)
self.controller = controller self.controller = controller
self.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
self.dimNode = ASDisplayNode() self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
@ -328,7 +376,7 @@ public final class InviteLinkInviteController: ViewController {
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction() 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) 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) self?.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in }, copyLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link)) UIPasteboard.general.string = invite.link
self?.controller?.present(shareController, in: .window(.root)) 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 }, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link)) let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root)) self?.controller?.present(shareController, in: .window(.root))
}, manageLinks: { [weak self] in }, manageLinks: { [weak self] in
let controller = inviteLinkListController(context: context, peerId: peerId) let controller = inviteLinkListController(context: context, peerId: peerId)
self?.controller?.push(controller) self?.controller?.parentNavigationController?.pushViewController(controller)
self?.controller?.dismiss() self?.controller?.dismiss()
}) })
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil) let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
let peerView = context.account.postbox.peerView(id: peerId) let peerView = context.account.postbox.peerView(id: peerId)
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView) self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, self.invitesContext.state)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view in |> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in
if let strongSelf = self { if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = [] 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 { 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 { } 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) let previousEntries = previousEntries.swap(entries)
@ -507,14 +577,14 @@ public final class InviteLinkInviteController: ViewController {
insets.bottom = layout.intrinsicInsets.bottom insets.bottom = layout.intrinsicInsets.bottom
let headerHeight: CGFloat = 54.0 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 layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let listTopInset = layoutTopInset + headerHeight let listTopInset = layoutTopInset + headerHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) 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 (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve) 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 { if result === self.headerNode.view {
return self.view return self.view
} }
if result === self.headerNode.view {
return self.view
}
if !self.bounds.contains(point) { if !self.bounds.contains(point) {
return nil return nil
} }
@ -569,8 +634,6 @@ public final class InviteLinkInviteController: ViewController {
case .changed: case .changed:
var translation = recognizer.translation(in: self.contentNode.view).y var translation = recognizer.translation(in: self.contentNode.view).y
if let currentPanOffset = self.panGestureArguments { if let currentPanOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 { if case let .known(value) = contentOffset, value <= 0.5 {
} else { } else {
translation = currentPanOffset translation = currentPanOffset

View File

@ -13,10 +13,12 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0 var sectionId: ItemListSectionId = 0
let theme: PresentationTheme let theme: PresentationTheme
let title: String
let text: String let text: String
init(theme: PresentationTheme, text: String) { init(theme: PresentationTheme, title: String, text: String) {
self.theme = theme self.theme = theme
self.title = title
self.text = text 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 { class InviteLinkInviteHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode private let titleNode: TextNode
private var animationNode: AnimatedStickerNode private let textNode: TextNode
private let iconBackgroundNode: ASImageNode
private let iconNode: ASImageNode
private var item: InviteLinkInviteHeaderItem? private var item: InviteLinkInviteHeaderItem?
init() { init() {
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode() self.textNode = TextNode()
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") { self.textNode.isUserInteractionEnabled = false
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true 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) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode) 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) { func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset let leftInset: CGFloat = 40.0 + params.leftInset
let topInset: CGFloat = 92.0 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) var updatedTheme: PresentationTheme?
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())) if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
let insets = itemListNeighborsGroupedInsets(neighbors) 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 return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string strongSelf.accessibilityLabel = attributedText.string
let iconSize = CGSize(width: 96.0, height: 96.0) if let _ = updatedTheme {
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize) strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 92.0, color: item.theme.actionSheet.controlAccentColor)
strongSelf.animationNode.updateLayout(size: iconSize) strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/LargeLink"), color: item.theme.list.itemCheckColors.foregroundColor)
}
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() 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) 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 { private final class InviteLinkListControllerArguments {
let context: AccountContext let context: AccountContext
let shareMainLink: (ExportedInvitation?) -> Void let shareMainLink: (ExportedInvitation) -> Void
let openMainLink: (ExportedInvitation?) -> Void let openMainLink: (ExportedInvitation) -> Void
let copyLink: (ExportedInvitation) -> Void
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let createLink: () -> Void let createLink: () -> Void
let openLink: (ExportedInvitation) -> Void let openLink: (ExportedInvitation) -> Void
let linkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void let linkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let deleteAllRevokedLinks: () -> 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.context = context
self.shareMainLink = shareMainLink self.shareMainLink = shareMainLink
self.openMainLink = openMainLink self.openMainLink = openMainLink
self.copyLink = copyLink
self.mainLinkContextAction = mainLinkContextAction self.mainLinkContextAction = mainLinkContextAction
self.createLink = createLink self.createLink = createLink
self.openLink = openLink self.openLink = openLink
@ -178,8 +180,14 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case let .mainLinkHeader(_, text): case let .mainLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .mainLink(_, invite, peers): case let .mainLink(_, invite, peers):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: { return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, displayButton: true, displayImporters: true, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
arguments.shareMainLink(invite) if let invite = invite {
arguments.copyLink(invite)
}
}, shareAction: {
if let invite = invite {
arguments.shareMainLink(invite)
}
}, contextAction: { node in }, contextAction: { node in
arguments.mainLinkContextAction(invite, node, nil) arguments.mainLinkContextAction(invite, node, nil)
}, viewAction: { }, viewAction: {
@ -194,7 +202,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.createLink() arguments.createLink()
}) })
case let .links(_, _, invites): 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) arguments.openLink(invite)
}, contextAction: { invite, node in }, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil) arguments.linkContextAction(invite, node, nil)
@ -208,7 +216,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.deleteAllRevokedLinks() arguments.deleteAllRevokedLinks()
}) })
case let .revokedLinks(_, _, invites): 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) arguments.openLink(invite)
}, contextAction: { invite, node in }, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil) arguments.linkContextAction(invite, node, nil)
@ -299,33 +307,21 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
let deleteAllRevokedLinksDisposable = MetaDisposable() let deleteAllRevokedLinksDisposable = MetaDisposable()
actionsDisposable.add(deleteAllRevokedLinksDisposable) 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?)? var getControllerImpl: (() -> ViewController?)?
let invitesPromise = Promise<ExportedInvitations?>() let invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
invitesPromise.set(.single(nil) let revokedInvitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: true, forceUpdate: true)
|> 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 arguments = InviteLinkListControllerArguments(context: context, shareMainLink: { invite in let arguments = InviteLinkListControllerArguments(context: context, shareMainLink: { invite in
if let invite = invite { let shareController = ShareController(context: context, subject: .url(invite.link))
let shareController = ShareController(context: context, subject: .url(invite.link)) presentControllerImpl?(shareController, nil)
presentControllerImpl?(shareController, nil)
}
}, openMainLink: { invite in }, openMainLink: { invite in
if let invite = invite { let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil) pushControllerImpl?(controller)
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 }, mainLinkContextAction: { invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else { guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
return return
@ -383,13 +379,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
} }
} }
if revoke { 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 updateState { state in
var updatedState = state var updatedState = state
updatedState.revokingPrivateLink = false updatedState.revokingPrivateLink = false
return updatedState 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) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController) presentInGlobalOverlayImpl?(contextController)
}, createLink: { }, createLink: {
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: { let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: { invite in
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false)) if let invite = invite {
invitesContext.add(invite)
}
}) })
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
pushControllerImpl?(controller) pushControllerImpl?(controller)
}, openLink: { invite in }, openLink: { invite in
if let invite = invite { 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) pushControllerImpl?(controller)
} }
}, linkContextAction: { invite, node, gesture in }, linkContextAction: { invite, node, gesture in
@ -445,8 +445,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
}, action: { _, f in }, action: { _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: { let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: { invite in
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false)) if let invite = invite {
if invite.isRevoked {
invitesContext.remove(invite)
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
} else {
invitesContext.update(invite)
}
}
}) })
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
pushControllerImpl?(controller) pushControllerImpl?(controller)
@ -465,13 +472,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
} }
controller.setItemGroups([ controller.setItemGroups([
ActionSheetItemGroup(items: [ ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text), ActionSheetTextItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
dismissAction() 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() })]) 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: { 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() })]) ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
@ -520,8 +532,9 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
dismissAction() dismissAction()
deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: { deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
})) }))
revokedInvitesContext.clear()
}) })
]), ]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) 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)) 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 |> deliverOnMainQueue
|> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in |> 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 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)) return (controllerState, (listState, arguments))
} }

View File

@ -209,7 +209,7 @@ public final class InviteLinkQRCodeController: ViewController {
self.contentContainerNode.addSubnode(self.qrIconNode) self.contentContainerNode.addSubnode(self.qrIconNode)
self.contentContainerNode.addSubnode(self.qrButtonNode) 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.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Info, font: textFont, textColor: secondaryTextColor)
self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share
@ -342,7 +342,6 @@ public final class InviteLinkQRCodeController: ViewController {
self.containerLayout = (layout, navigationBarHeight) self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input]) var insets = layout.insets(options: [.statusBar, .input])
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top) insets.top = max(10.0, insets.top)
let makeImageLayout = self.qrImageNode.asyncLayout() let makeImageLayout = self.qrImageNode.asyncLayout()
@ -352,7 +351,7 @@ public final class InviteLinkQRCodeController: ViewController {
let _ = imageApply() 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.qrImageNode, frame: imageFrame)
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame) transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
@ -365,7 +364,7 @@ public final class InviteLinkQRCodeController: ViewController {
let inset: CGFloat = 22.0 let inset: CGFloat = 22.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) 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) transition.updateFrame(node: self.textNode, frame: textFrame)
let buttonSideInset: CGFloat = 16.0 let buttonSideInset: CGFloat = 16.0
@ -379,7 +378,7 @@ public final class InviteLinkQRCodeController: ViewController {
let titleHeight: CGFloat = 54.0 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) 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) transition.updateFrame(node: self.titleNode, frame: titleFrame)
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) 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) transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0 let buttonInset: CGFloat = 16.0

View File

@ -24,12 +24,14 @@ import DirectionalPanGesture
class InviteLinkViewInteraction { class InviteLinkViewInteraction {
let context: AccountContext let context: AccountContext
let openPeer: (PeerId) -> Void let openPeer: (PeerId) -> Void
let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void
let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> 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.context = context
self.openPeer = openPeer self.openPeer = openPeer
self.copyLink = copyLink
self.shareLink = shareLink self.shareLink = shareLink
self.contextAction = contextAction self.contextAction = contextAction
} }
@ -72,7 +74,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case creatorHeader(PresentationTheme, String) case creatorHeader(PresentationTheme, String)
case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32) case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
case importerHeader(PresentationTheme, String) case importerHeader(PresentationTheme, String)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32) case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32, Bool)
var stableId: InviteLinkViewEntryId { var stableId: InviteLinkViewEntryId {
switch self { switch self {
@ -84,7 +86,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
return .creator return .creator
case .importerHeader: case .importerHeader:
return .importerHeader return .importerHeader
case let .importer(_, _, _, peer, _): case let .importer(_, _, _, peer, _, _):
return .importer(peer.id) return .importer(peer.id)
} }
} }
@ -115,8 +117,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate): case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate { 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 return true
} else { } else {
return false return false
@ -154,11 +156,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case .creator, .importer: case .creator, .importer:
return true return true
} }
case let .importer(lhsIndex, _, _, _, _): case let .importer(lhsIndex, _, _, _, _, _):
switch rhs { switch rhs {
case .link, .creatorHeader, .creator, .importerHeader: case .link, .creatorHeader, .creator, .importerHeader:
return false return false
case let .importer(rhsIndex, _, _, _, _): case let .importer(rhsIndex, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} }
} }
@ -168,7 +170,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
switch self { switch self {
case let .link(_, invite): case let .link(_, invite):
let buttonColor = color(for: 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) interaction.shareLink(invite)
}, contextAction: { node in }, contextAction: { node in
interaction.contextAction(invite, node, nil) interaction.contextAction(invite, node, nil)
@ -183,11 +188,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
case let .importerHeader(_, title): case let .importerHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: 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) 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: { 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) 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 context: AccountContext
private let peerId: PeerId private let peerId: PeerId
private let invite: ExportedInvitation private let invite: ExportedInvitation
private let invitationsContext: PeerExportedInvitationsContext?
private let revokedInvitationsContext: PeerExportedInvitationsContext?
private let importersContext: PeerInvitationImportersContext? private let importersContext: PeerInvitationImportersContext?
private var presentationDataDisposable: Disposable? 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.context = context
self.peerId = peerId self.peerId = peerId
self.invite = invite self.invite = invite
self.invitationsContext = invitationsContext
self.revokedInvitationsContext = revokedInvitationsContext
self.importersContext = importersContext self.importersContext = importersContext
super.init(navigationBarPresentationData: nil) super.init(navigationBarPresentationData: nil)
@ -292,12 +301,16 @@ public final class InviteLinkViewController: ViewController {
private let peerId: PeerId private let peerId: PeerId
private let invite: ExportedInvitation private let invite: ExportedInvitation
private let importersContext: PeerInvitationImportersContext
private var interaction: InviteLinkViewInteraction? private var interaction: InviteLinkViewInteraction?
private var presentationData: PresentationData private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData> private let presentationDataPromise: Promise<PresentationData>
private var presentationDataDisposable: Disposable?
private var disposable: Disposable? private var disposable: Disposable?
private let actionDisposable = MetaDisposable()
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
private let contentNode: ASDisplayNode private let contentNode: ASDisplayNode
@ -316,10 +329,6 @@ public final class InviteLinkViewController: ViewController {
private var validLayout: ContainerViewLayout? private var validLayout: ContainerViewLayout?
private var presentationDataDisposable: Disposable?
private let importersContext: PeerInvitationImportersContext
init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?, controller: InviteLinkViewController) { init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?, controller: InviteLinkViewController) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
@ -382,6 +391,10 @@ public final class InviteLinkViewController: ViewController {
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always)) 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 }, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link)) let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root)) 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)) 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 items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, 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(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, invite.date)) 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())) entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased()))
} }
var index: Int32 = 0 var index: Int32 = 0
for importer in state.importers { if state.importers.isEmpty && state.isLoadingMore {
if let peer = importer.peer.peer { let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date)) 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) let previousEntries = previousEntries.swap(entries)
@ -514,8 +560,17 @@ public final class InviteLinkViewController: ViewController {
let navigationController = self.controller?.navigationController as? NavigationController let navigationController = self.controller?.navigationController as? NavigationController
self.controller?.dismiss() self.controller?.dismiss()
let invitationsContext = self.controller?.invitationsContext
if let navigationController = navigationController { 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 controller.navigationPresentation = .modal
navigationController.pushViewController(controller) navigationController.pushViewController(controller)
} }
@ -638,11 +693,6 @@ public final class InviteLinkViewController: ViewController {
if result === self.headerNode.view { if result === self.headerNode.view {
return self.view return self.view
} }
if result === self.headerNode.view {
return self.view
}
if !self.bounds.contains(point) { if !self.bounds.contains(point) {
return nil return nil
} }

View File

@ -42,10 +42,60 @@ func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit)) let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
availability = min(fraction, availability) 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 class ItemNode: ASDisplayNode {
private let selectionNode: HighlightTrackingButtonNode
private let wrapperNode: ASDisplayNode
private let backgroundNode: ASImageNode private let backgroundNode: ASImageNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
@ -59,19 +109,28 @@ private class ItemNode: ASDisplayNode {
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
private let subtitleNode: 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 action: (() -> Void)?
var contextAction: ((ASDisplayNode) -> Void)? var contextAction: ((ASDisplayNode) -> Void)?
private let hapticFeedback = HapticFeedback()
override init() { override init() {
self.selectionNode = HighlightTrackingButtonNode()
self.wrapperNode = ASDisplayNode()
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode() self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.buttonNode = HighlightTrackingButtonNode() self.buttonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode() self.extractedContainerNode = ContextExtractedContentContainingNode()
@ -80,37 +139,44 @@ private class ItemNode: ASDisplayNode {
self.buttonIconNode = ASImageNode() self.buttonIconNode = ASImageNode()
self.buttonIconNode.displaysAsynchronously = false self.buttonIconNode.displaysAsynchronously = false
self.buttonIconNode.displayWithoutProcessing = true 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 = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 2 self.titleNode.maximumNumberOfLines = 2
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextNode() self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1 self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.isUserInteractionEnabled = false
super.init() super.init()
self.addSubnode(self.backgroundNode) self.addSubnode(self.wrapperNode)
self.addSubnode(self.iconNode) self.wrapperNode.addSubnode(self.backgroundNode)
self.wrapperNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.extractedContainerNode) self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode) self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.buttonNode.addSubnode(self.containerNode) self.buttonNode.addSubnode(self.containerNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode) self.wrapperNode.addSubnode(self.selectionNode)
self.addSubnode(self.subtitleNode) 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.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in self.buttonNode.highligthedChanged = { [weak self] highlighted in
@ -126,11 +192,12 @@ private class ItemNode: ASDisplayNode {
} }
} }
override func didLoad() { deinit {
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.updateTimer?.invalidate()
} }
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { @objc private func tapped() {
self.hapticFeedback.impact(.light)
self.action?() self.action?()
} }
@ -138,35 +205,66 @@ private class ItemNode: ASDisplayNode {
self.contextAction?(self.extractedContainerNode) self.contextAction?(self.extractedContainerNode)
} }
func update(size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize { func update(size: CGSize, wide: Bool, share: 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)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let availability = invitationAvailability(invite)
var isExpired = false let availability = invitationAvailability(invite)
let secondaryTextColor: UIColor let color: ItemBackgroundColor
if invite.isRevoked { if invite.isRevoked {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xd4d8db).cgColor, UIColor(rgb: 0xced2d5).cgColor]) color = .gray
secondaryTextColor = UIColor(rgb: 0xf8f9f9)
} else if invite.expireDate == nil && invite.usageLimit == nil { } else if invite.expireDate == nil && invite.usageLimit == nil {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x00b5f7).cgColor, UIColor(rgb: 0x00b2f6).cgColor]) color = .blue
secondaryTextColor = UIColor(rgb: 0xa7f4ff)
} else if availability >= 0.5 { } else if availability >= 0.5 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x4aca62).cgColor, UIColor(rgb: 0x43c85c).cgColor]) color = .green
secondaryTextColor = UIColor(rgb: 0xc5ffe6)
} else if availability > 0.0 { } else if availability > 0.0 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf8a953).cgColor, UIColor(rgb: 0xf7a64e).cgColor]) color = .yellow
secondaryTextColor = UIColor(rgb: 0xfeffd7)
} else { } else {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf2656a).cgColor, UIColor(rgb: 0xf25f65).cgColor]) color = .red
secondaryTextColor = UIColor(rgb: 0xffd3de)
isExpired = true
} }
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: "") var inviteLink = invite.link.replacingOccurrences(of: "https://", with: "")
if !wide { if !wide {
inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n") inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n")
@ -180,11 +278,13 @@ private class ItemNode: ASDisplayNode {
} }
self.titleNode.attributedText = title self.titleNode.attributedText = title
self.buttonIconNode.image = share ? shareIcon : moreIcon
var subtitleText: String = "" var subtitleText: String = ""
if let count = invite.count { if let count = invite.count {
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count) subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
} else { } 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 invite.isRevoked {
if !subtitleText.isEmpty { if !subtitleText.isEmpty {
@ -194,11 +294,27 @@ private class ItemNode: ASDisplayNode {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode() self.timerNode?.removeFromSupernode()
self.timerNode = nil self.timerNode = nil
} else if let expireDate = invite.expireDate, currentTime > expireDate { } else if let expireDate = invite.expireDate, currentTime >= expireDate {
if !subtitleText.isEmpty { if !subtitleText.isEmpty {
subtitleText += "" 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.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode() self.timerNode?.removeFromSupernode()
self.timerNode = nil self.timerNode = nil
@ -209,21 +325,27 @@ private class ItemNode: ASDisplayNode {
timerNode = current timerNode = current
} else { } else {
timerNode = TimerNode() timerNode = TimerNode()
timerNode.isUserInteractionEnabled = false
self.timerNode = timerNode self.timerNode = timerNode
self.addSubnode(timerNode) self.addSubnode(timerNode)
} }
timerNode.update(color: UIColor.white, creationTimestamp: invite.date, deadlineTimestamp: expireDate) timerNode.update(color: UIColor.white, creationTimestamp: invite.date, deadlineTimestamp: expireDate)
if share {
subtitleText = presentationData.strings.InviteLink_TapToCopy
}
} else { } else {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)
self.timerNode?.removeFromSupernode() self.timerNode?.removeFromSupernode()
self.timerNode = nil 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.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) 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 = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
self.subtitleNode.attributedText = subtitle
let titleSize = self.titleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0)) 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)) 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 itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: itemSize) 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.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.selectionNode, frame: backgroundFrame)
let buttonSize = CGSize(width: 26.0, height: 26.0) 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) 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 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 self.items = items
var contentSize: CGSize = size var contentSize: CGSize = size
@ -288,7 +412,7 @@ class InviteLinksGridNode: ASDisplayNode {
let col = CGFloat(i % 2) let col = CGFloat(i % 2)
let row = floor(CGFloat(i) / 2.0) let row = floor(CGFloat(i) / 2.0)
let wide = (i == self.items.count - 1 && (self.items.count % 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) var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 4.0 + row * (122.0 + itemSpacing)), size: itemSize)
if !wide && col > 0 { if !wide && col > 0 {
itemFrame.origin.x += itemSpacing + itemSize.width itemFrame.origin.x += itemSpacing + itemSize.width
@ -408,7 +532,7 @@ private final class TimerNode: ASDisplayNode {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp) 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? let image: UIImage?
@ -424,44 +548,47 @@ private final class TimerNode: ASDisplayNode {
let startAngle: CGFloat = -CGFloat.pi / 2.0 let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) let sparks = fraction > 0.1 && fraction != 1.0
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) 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 let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = [] var removeIndices: [Int] = []
for i in 0 ..< self.particles.count { for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime { if currentTime > self.particles[i].lifetime {
removeIndices.append(i) removeIndices.append(i)
} else { } else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position var p = self.particles[i].position
let d = self.particles[i].direction let d = self.particles[i].direction
let v = self.particles[i].velocity let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p self.particles[i].position = p
}
} }
}
for i in removeIndices.reversed() { for i in removeIndices.reversed() {
self.particles.remove(at: i) self.particles.remove(at: i)
} }
let newParticleCount = 1 let newParticleCount = 1
for _ in 0 ..< newParticleCount { for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0 let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
let angle: CGFloat = degrees * CGFloat.pi / 180.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 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 velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) 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) let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle) self.particles.append(particle)
}
} }
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in 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.addPath(path)
context.strokePath() context.strokePath()
for particle in self.particles { if sparks {
let size: CGFloat = 2.0 for particle in self.particles {
context.setAlpha(particle.alpha) let size: CGFloat = 2.0
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))) 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 bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode private let maskNode: ASImageNode
private let datePicker: UIDatePicker private var datePicker: UIDatePicker?
private var item: ItemListDatePickerItem? private var item: ItemListDatePickerItem?
@ -98,25 +98,31 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true 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) super.init(layerBacked: false, dynamicBounce: false)
self.datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
} }
public override func didLoad() { public override func didLoad() {
super.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() { @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) { 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.backgroundNode.backgroundColor = itemBackgroundColor
} }
strongSelf.datePicker.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date() 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?.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
switch item.style { switch item.style {
case .plain: case .plain:

View File

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

View File

@ -30,9 +30,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let invite: ExportedInvitation? let invite: ExportedInvitation?
let peers: [Peer] let peers: [Peer]
let displayButton: Bool
let displayImporters: Bool
let buttonColor: UIColor? let buttonColor: UIColor?
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let copyAction: (() -> Void)?
let shareAction: (() -> Void)? let shareAction: (() -> Void)?
let contextAction: ((ASDisplayNode) -> Void)? let contextAction: ((ASDisplayNode) -> Void)?
let viewAction: (() -> Void)? let viewAction: (() -> Void)?
@ -43,9 +46,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
presentationData: ItemListPresentationData, presentationData: ItemListPresentationData,
invite: ExportedInvitation?, invite: ExportedInvitation?,
peers: [Peer], peers: [Peer],
displayButton: Bool,
displayImporters: Bool,
buttonColor: UIColor?, buttonColor: UIColor?,
sectionId: ItemListSectionId, sectionId: ItemListSectionId,
style: ItemListStyle, style: ItemListStyle,
copyAction: (() -> Void)?,
shareAction: (() -> Void)?, shareAction: (() -> Void)?,
contextAction: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode) -> Void)?,
viewAction: (() -> Void)?, viewAction: (() -> Void)?,
@ -55,9 +61,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.presentationData = presentationData self.presentationData = presentationData
self.invite = invite self.invite = invite
self.peers = peers self.peers = peers
self.displayButton = displayButton
self.displayImporters = displayImporters
self.buttonColor = buttonColor self.buttonColor = buttonColor
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.copyAction = copyAction
self.shareAction = shareAction self.shareAction = shareAction
self.contextAction = contextAction self.contextAction = contextAction
self.viewAction = viewAction self.viewAction = viewAction
@ -108,6 +117,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
private let fieldNode: ASImageNode private let fieldNode: ASImageNode
private let addressNode: TextNode private let addressNode: TextNode
private let fieldButtonNode: HighlightTrackingButtonNode
private let extractedContainerNode: ContextExtractedContentContainingNode private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode private let containerNode: ContextControllerSourceNode
private let addressButtonNode: HighlightTrackingButtonNode private let addressButtonNode: HighlightTrackingButtonNode
@ -152,11 +162,14 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.addressNode = TextNode() self.addressNode = TextNode()
self.addressNode.isUserInteractionEnabled = false self.addressNode.isUserInteractionEnabled = false
self.fieldButtonNode = HighlightTrackingButtonNode()
self.addressButtonNode = HighlightTrackingButtonNode() self.addressButtonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode() self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false self.containerNode.isGestureEnabled = false
self.addressButtonIconNode = ASImageNode() self.addressButtonIconNode = ASImageNode()
self.addressButtonIconNode.contentMode = .center
self.addressButtonIconNode.displaysAsynchronously = false self.addressButtonIconNode.displaysAsynchronously = false
self.addressButtonIconNode.displayWithoutProcessing = true self.addressButtonIconNode.displayWithoutProcessing = true
@ -171,6 +184,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.addSubnode(self.fieldNode) self.addSubnode(self.fieldNode)
self.addSubnode(self.addressNode) self.addSubnode(self.addressNode)
self.addSubnode(self.fieldButtonNode)
self.addSubnode(self.avatarsNode) self.addSubnode(self.avatarsNode)
self.addSubnode(self.invitedPeersNode) self.addSubnode(self.invitedPeersNode)
self.addSubnode(self.avatarsButtonNode) self.addSubnode(self.avatarsButtonNode)
@ -183,6 +197,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.addSubnode(self.activateArea) 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.addTarget(self, action: #selector(self.addressButtonPressed), forControlEvents: .touchUpInside)
self.addressButtonNode.highligthedChanged = { [weak self] highlighted in self.addressButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self { if let strongSelf = self {
@ -218,6 +245,12 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
self.avatarsButtonNode.addTarget(self, action: #selector(self.avatarsButtonPressed), forControlEvents: .touchUpInside) 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() { @objc private func addressButtonPressed() {
if let item = self.item { if let item = self.item {
item.contextAction?(self.extractedContainerNode) item.contextAction?(self.extractedContainerNode)
@ -287,7 +320,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
switch item.style { switch item.style {
case .plain: case .plain:
height -= 57.0
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = .clear itemSeparatorColor = .clear
insets = UIEdgeInsets() insets = UIEdgeInsets()
@ -296,6 +328,14 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors) insets = itemListNeighborsGroupedInsets(neighbors)
} }
if !item.displayImporters {
height -= 57.0
}
if !item.displayButton {
height -= 63.0
}
contentSize = CGSize(width: params.width, height: height) contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) 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)) let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight))
strongSelf.fieldNode.frame = fieldFrame 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.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.frame = strongSelf.addressButtonNode.bounds
strongSelf.extractedContainerNode.contentRect = strongSelf.addressButtonNode.bounds strongSelf.extractedContainerNode.contentRect = strongSelf.addressButtonNode.bounds
strongSelf.addressButtonIconNode.frame = 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.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.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 { public enum ItemListDisclosureLabelStyle {
case text case text
case detailText case detailText
case coloredText(UIColor)
case multilineDetailText case multilineDetailText
case badge(UIColor) case badge(UIColor)
case color(UIColor) case color(UIColor)
@ -277,6 +278,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = detailFont labelFont = detailFont
labelConstrain = params.width - params.rightInset - 40.0 - leftInset labelConstrain = params.width - params.rightInset - 40.0 - leftInset
case let .coloredText(color):
labelBadgeColor = color
labelFont = titleFont
default: default:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont labelFont = titleFont

View File

@ -46,6 +46,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let clearType: ItemListSingleLineInputClearType let clearType: ItemListSingleLineInputClearType
let maxLength: Int let maxLength: Int
let enabled: Bool let enabled: Bool
let selectAllOnFocus: Bool
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let action: () -> Void let action: () -> Void
let textUpdated: (String) -> Void let textUpdated: (String) -> Void
@ -55,7 +56,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let cleared: (() -> Void)? let cleared: (() -> Void)?
public let tag: ItemListItemTag? 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.presentationData = presentationData
self.title = title self.title = title
self.text = text self.text = text
@ -67,6 +68,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
self.clearType = clearType self.clearType = clearType
self.maxLength = maxLength self.maxLength = maxLength
self.enabled = enabled self.enabled = enabled
self.selectAllOnFocus = selectAllOnFocus
self.tag = tag self.tag = tag
self.sectionId = sectionId self.sectionId = sectionId
self.textUpdated = textUpdated self.textUpdated = textUpdated
@ -494,6 +496,13 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
@objc public func textFieldDidBeginEditing(_ textField: UITextField) { @objc public func textFieldDidBeginEditing(_ textField: UITextField) {
self.item?.updatedFocus?(true) 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() self.updateClearButtonVisibility()
} }

View File

@ -368,17 +368,17 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelAdminControllerArguments let arguments = arguments as! ChannelAdminControllerArguments
switch self { 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 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: { }, avatarTapped: {
}) })
case let .rankTitle(theme, text, count, limit): case let .rankTitle(_, text, count, limit):
var accessoryText: ItemListSectionHeaderAccessoryText? var accessoryText: ItemListSectionHeaderAccessoryText?
if let count = count { if let count = count {
accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic) accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic)
} }
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) 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 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) arguments.updateRank(text, updatedText)
}, shouldUpdateText: { text in }, shouldUpdateText: { text in
@ -392,23 +392,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
}, action: { }, action: {
arguments.dismissInput() arguments.dismissInput()
}) })
case let .rankInfo(theme, text): case let .rankInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) 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) 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 return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggleRight(right, flags) arguments.toggleRight(right, flags)
}, activatedWhileDisabled: { }, activatedWhileDisabled: {
arguments.toggleRightWhileDisabled(right, flags) arguments.toggleRightWhileDisabled(right, flags)
}) })
case let .addAdminsInfo(theme, text): case let .addAdminsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) 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: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.transferOwnership() arguments.transferOwnership()
}, tag: nil) }, 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: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.dismissAdmin() arguments.dismissAdmin()
}, tag: nil) }, tag: nil)
@ -1004,12 +1004,12 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
var currentRank: String? var currentRank: String?
var currentFlags: TelegramChatAdminRightsFlags? var currentFlags: TelegramChatAdminRightsFlags?
switch initialParticipant { switch initialParticipant {
case let .creator(creator): case let .creator(_, adminInfo, rank):
currentRank = creator.rank currentRank = rank
currentFlags = maskRightsFlags currentFlags = adminInfo?.rights.flags ?? maskRightsFlags.subtracting(.canBeAnonymous)
case let .member(member): case let .member(_, _, adminInfo, _, rank):
if updateFlags == nil { if updateFlags == nil {
if member.adminInfo?.rights == nil { if adminInfo?.rights == nil {
if channel.flags.contains(.isCreator) { if channel.flags.contains(.isCreator) {
updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous])
} else if let adminRights = channel.adminRights { } else if let adminRights = channel.adminRights {
@ -1019,8 +1019,8 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
} }
} }
} }
currentRank = member.rank currentRank = rank
currentFlags = member.adminInfo?.rights.flags currentFlags = adminInfo?.rights.flags
} }
let effectiveRank = updateRank ?? currentRank let effectiveRank = updateRank ?? currentRank

View File

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

View File

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

View File

@ -3,20 +3,24 @@ import Postbox
public final class CachedStickerQueryResult: PostboxCoding { public final class CachedStickerQueryResult: PostboxCoding {
public let items: [TelegramMediaFile] public let items: [TelegramMediaFile]
public let hash: Int32 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.items = items
self.hash = hash self.hash = hash
self.timestamp = timestamp
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.items = decoder.decodeObjectArrayForKey("it").map { $0 as! TelegramMediaFile } self.items = decoder.decodeObjectArrayForKey("it").map { $0 as! TelegramMediaFile }
self.hash = decoder.decodeInt32ForKey("h", orElse: 0) self.hash = decoder.decodeInt32ForKey("h", orElse: 0)
self.timestamp = decoder.decodeInt32ForKey("t", orElse: 0)
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.items, forKey: "it") encoder.encodeObjectArray(self.items, forKey: "it")
encoder.encodeInt32(self.hash, forKey: "h") encoder.encodeInt32(self.hash, forKey: "h")
encoder.encodeInt32(self.timestamp, forKey: "t")
} }
public static func cacheKey(_ query: String) -> ValueBoxKey { 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 { 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 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 cachedContextResults: Int8 = 10
public static let proximityNotificationStoredState: Int8 = 11 public static let proximityNotificationStoredState: Int8 = 11
public static let cachedPeerInvitationImporters: Int8 = 12 public static let cachedPeerInvitationImporters: Int8 = 12
public static let cachedPeerExportedInvitations: Int8 = 13
} }
public struct UnorderedItemList { 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() let buffer = Buffer()
buffer.appendInt32(1838984707) buffer.appendInt32(1785900140)
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true) peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {adminId!.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)} if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
serializeInt32(limit, 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) let reader = BufferReader(buffer)
var result: Api.messages.ExportedChatInvites? var result: Api.messages.ExportedChatInvites?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -1061,24 +1061,6 @@ public final class VoiceChatController: ViewController {
strongSelf.accountPeer = accountPeer strongSelf.accountPeer = accountPeer
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) 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.didSetDataReady = true
strongSelf.controller?.dataReady.set(true) strongSelf.controller?.dataReady.set(true)
} }

View File

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

View File

@ -6,53 +6,45 @@ import MtProtoKit
import SyncCore 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 return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
let flags: Int32 = (1 << 2) let flags: Int32 = (1 << 2)
if let _ = peer as? TelegramChannel { if let _ = peer as? TelegramChannel {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, cachedData.exportedInvitation != nil && !revokeExisted { return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
return .single(cachedData.exportedInvitation) |> retryRequest
} else { |> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil)) return account.postbox.transaction { transaction -> ExportedInvitation? in
|> retryRequest if let invitation = ExportedInvitation(apiExportedInvite: result) {
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
return account.postbox.transaction { transaction -> ExportedInvitation? in if let current = current as? CachedChannelData {
if let invitation = ExportedInvitation(apiExportedInvite: result) { return current.withUpdatedExportedInvitation(invitation)
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in } else {
if let current = current as? CachedChannelData { return CachedChannelData().withUpdatedExportedInvitation(invitation)
return current.withUpdatedExportedInvitation(invitation) }
} else { })
return CachedChannelData().withUpdatedExportedInvitation(invitation) return invitation
} } else {
}) return nil
return invitation
} else {
return nil
}
} }
} }
} }
} else if let _ = peer as? TelegramGroup { } else if let _ = peer as? TelegramGroup {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData, cachedData.exportedInvitation != nil && !revokeExisted { return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
return .complete() |> retryRequest
} else { |> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil)) return account.postbox.transaction { transaction -> ExportedInvitation? in
|> retryRequest if let invitation = ExportedInvitation(apiExportedInvite: result) {
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
return account.postbox.transaction { transaction -> ExportedInvitation? in if let current = current as? CachedGroupData {
if let invitation = ExportedInvitation(apiExportedInvite: result) { return current.withUpdatedExportedInvitation(invitation)
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in } else {
if let current = current as? CachedGroupData { return current
return current.withUpdatedExportedInvitation(invitation) }
} else { })
return current return invitation
} } else {
}) return nil
return invitation
} else {
return nil
}
} }
} }
} }
@ -65,8 +57,12 @@ public func ensuredExistingPeerExportedInvitation(account: Account, peerId: Peer
} |> switchToLatest } |> switchToLatest
} }
public func createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal<ExportedInvitation?, NoError> { public enum CreatePeerExportedInvitationError {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in 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) { if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0 var flags: Int32 = 0
if let _ = expireDate { if let _ = expireDate {
@ -76,7 +72,7 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
flags |= (1 << 1) flags |= (1 << 1)
} }
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit)) 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 |> map { result -> ExportedInvitation? in
if let invitation = ExportedInvitation(apiExportedInvite: result) { if let invitation = ExportedInvitation(apiExportedInvite: result) {
return invitation return invitation
@ -87,59 +83,9 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
} else { } else {
return .complete() return .complete()
} }
} |> switchToLatest }
} |> castError(CreatePeerExportedInvitationError.self)
|> 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
} }
public enum EditPeerExportedInvitationError { public enum EditPeerExportedInvitationError {
@ -218,6 +164,59 @@ public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link:
|> switchToLatest |> 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 { public enum DeletePeerExportedInvitationError {
case generic case generic
} }
@ -251,6 +250,320 @@ public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: Pe
|> switchToLatest |> 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) private let cachedPeerInvitationImportersCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
public struct PeerInvitationImportersState: Equatable { public struct PeerInvitationImportersState: Equatable {
@ -262,7 +575,7 @@ public struct PeerInvitationImportersState: Equatable {
public var isLoadingMore: Bool public var isLoadingMore: Bool
public var hasLoadedOnce: Bool public var hasLoadedOnce: Bool
public var canLoadMore: Bool public var canLoadMore: Bool
public var count: Int public var count: Int32
} }
final class CachedPeerInvitationImporters: PostboxCoding { final class CachedPeerInvitationImporters: PostboxCoding {
@ -330,8 +643,9 @@ private final class PeerInvitationImportersContextImpl {
private var isLoadingMore: Bool = false private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true private var canLoadMore: Bool = true
private var loadedFromCache = false
private var results: [PeerInvitationImportersState.Importer] = [] private var results: [PeerInvitationImportersState.Importer] = []
private var count: Int private var count: Int32
private var populateCache: Bool = true private var populateCache: Bool = true
let state = Promise<PeerInvitationImportersState>() let state = Promise<PeerInvitationImportersState>()
@ -342,7 +656,7 @@ private final class PeerInvitationImportersContextImpl {
self.peerId = peerId self.peerId = peerId
self.link = invite.link self.link = invite.link
let count = invite.count.flatMap { Int($0) } ?? 0 let count = invite.count ?? 0
self.count = count self.count = count
self.isLoadingMore = true self.isLoadingMore = true
@ -371,6 +685,7 @@ private final class PeerInvitationImportersContextImpl {
strongSelf.results = cachedPeers strongSelf.results = cachedPeers
strongSelf.hasLoadedOnce = true strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = canLoadMore strongSelf.canLoadMore = canLoadMore
strongSelf.loadedFromCache = true
} }
strongSelf.loadMore() strongSelf.loadMore()
})) }))
@ -390,12 +705,18 @@ private final class PeerInvitationImportersContextImpl {
let account = self.account let account = self.account
let peerId = self.peerId let peerId = self.peerId
let link = self.link let link = self.link
let lastResult = self.results.last
let populateCache = self.populateCache 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 self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer) 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 { if let inputPeer = inputPeer {
let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty
let offsetDate = lastResult?.date ?? 0 let offsetDate = lastResult?.date ?? 0
@ -404,8 +725,8 @@ private final class PeerInvitationImportersContextImpl {
|> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in |> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in
return .single(nil) return .single(nil)
} }
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in |> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int) in return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int32) in
guard let result = result else { guard let result = result else {
return ([], 0) return ([], 0)
} }
@ -434,7 +755,7 @@ private final class PeerInvitationImportersContextImpl {
if populateCache { 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) 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.hasLoadedOnce = true
strongSelf.canLoadMore = !importers.isEmpty strongSelf.canLoadMore = !importers.isEmpty
if strongSelf.canLoadMore { if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, strongSelf.results.count) strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
} else { } else {
strongSelf.count = strongSelf.results.count strongSelf.count = Int32(strongSelf.results.count)
} }
strongSelf.updateState() strongSelf.updateState()
})) }))

View File

@ -14,6 +14,14 @@ public struct TelegramPeerPhoto {
public let index: Int public let index: Int
public let totalCount: Int public let totalCount: Int
public let messageId: MessageId? 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> { public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> {

View File

@ -5,6 +5,26 @@ import SwiftSignalKit
import SyncCore 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 final class FoundStickerItem: Equatable {
public let file: TelegramMediaFile public let file: TelegramMediaFile
public let stringRepresentations: [String] public let stringRepresentations: [String]
@ -140,7 +160,15 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
result.append(contentsOf: installedItems) 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) return (result, cached)
} |> mapToSignal { localItems, cached -> Signal<[FoundStickerItem], NoError> in } |> 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: animatedItems)
result.append(contentsOf: items) 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 return result
case .stickersNotModified: case .stickersNotModified:

View File

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

View File

@ -205,10 +205,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
let peerStatusSettings = PeerStatusSettings(apiSettings: userFull.settings) let peerStatusSettings = PeerStatusSettings(apiSettings: userFull.settings)
var hasScheduledMessages = false let hasScheduledMessages = (userFull.flags & 1 << 12) != 0
if (userFull.flags & 1 << 12) != 0 {
hasScheduledMessages = true
}
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) 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 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) }) let pinnedMessageId = chatFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) })
var peers: [Peer] = [] 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 applicationInForegroundDisposable: Disposable?
private var applicationInFocusDisposable: Disposable? private var applicationInFocusDisposable: Disposable?
private let checksTooltipDisposable = MetaDisposable()
private var shouldDisplayChecksTooltip = false
private var checkedPeerChatServiceActions = false private var checkedPeerChatServiceActions = false
private var willAppear = false private var willAppear = false
@ -3424,6 +3427,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.peekTimerDisposable.dispose() self.peekTimerDisposable.dispose()
self.hasActiveGroupCallDisposable?.dispose() self.hasActiveGroupCallDisposable?.dispose()
self.createVoiceChatDisposable.dispose() self.createVoiceChatDisposable.dispose()
self.checksTooltipDisposable.dispose()
} }
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { 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 text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText))
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
var entitiesAttribute: TextEntitiesMessageAttribute? var entitiesAttribute: TextEntitiesMessageAttribute?
@ -6236,7 +6239,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.openScheduledMessages() 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 return false
} }
if !strongSelf.context.sharedContext.currentMediaInputSettings.with { $0.enableRaiseToSpeak } { if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) {
return false 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 { if self.scheduledActivateInput {
self.scheduledActivateInput = false self.scheduledActivateInput = false
@ -7481,7 +7499,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|> deliverOnMainQueue).start(next: { [weak self, weak controller] result in |> deliverOnMainQueue).start(next: { [weak self, weak controller] result in
controller?.dismiss() 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 return
} }
let presentationData = strongSelf.presentationData let presentationData = strongSelf.presentationData
@ -7682,16 +7700,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { 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 self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var state = state var state = state
if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty {
state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference)) state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
} }
if !desc.text.isEmpty { if !text.isEmpty {
state = state.updatedInterfaceState { state in state = state.updatedInterfaceState { state in
if let editMessage = state.editMessage { 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 return state
} }
@ -9338,7 +9356,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let complete = results.completed
var navigateIndex: MessageIndex? var navigateIndex: MessageIndex?
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search { if let data = current.search {
@ -10113,8 +10130,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
case let .withBotStartPayload(botStart): case let .withBotStartPayload(botStart):
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(peerId), botStart: botStart)) self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(peerId), botStart: botStart))
default:
break
} }
} }
} else { } else {
@ -10536,7 +10551,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) { private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) {
self.commitPurposefulAction() self.commitPurposefulAction()
self.presentVoiceMessageDiscardAlert(action: { let _ = self.presentVoiceMessageDiscardAlert(action: {
if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback { if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback {
if url.hasSuffix(".m3u8") { if url.hasSuffix(".m3u8") {
let navigationController = self.navigationController as? NavigationController let navigationController = self.navigationController as? NavigationController

View File

@ -159,6 +159,7 @@ final class PeerInfoScreenData {
let members: PeerInfoMembersData? let members: PeerInfoMembersData?
let encryptionKeyFingerprint: SecretChatKeyFingerprint? let encryptionKeyFingerprint: SecretChatKeyFingerprint?
let globalSettings: TelegramGlobalSettings? let globalSettings: TelegramGlobalSettings?
let invitations: PeerExportedInvitationsState?
init( init(
peer: Peer?, peer: Peer?,
@ -172,7 +173,8 @@ final class PeerInfoScreenData {
linkedDiscussionPeer: Peer?, linkedDiscussionPeer: Peer?,
members: PeerInfoMembersData?, members: PeerInfoMembersData?,
encryptionKeyFingerprint: SecretChatKeyFingerprint?, encryptionKeyFingerprint: SecretChatKeyFingerprint?,
globalSettings: TelegramGlobalSettings? globalSettings: TelegramGlobalSettings?,
invitations: PeerExportedInvitationsState?
) { ) {
self.peer = peer self.peer = peer
self.cachedData = cachedData self.cachedData = cachedData
@ -186,6 +188,7 @@ final class PeerInfoScreenData {
self.members = members self.members = members
self.encryptionKeyFingerprint = encryptionKeyFingerprint self.encryptionKeyFingerprint = encryptionKeyFingerprint
self.globalSettings = globalSettings self.globalSettings = globalSettings
self.invitations = invitations
} }
} }
@ -442,7 +445,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account
linkedDiscussionPeer: nil, linkedDiscussionPeer: nil,
members: nil, members: nil,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: globalSettings globalSettings: globalSettings,
invitations: nil
) )
} }
} }
@ -464,7 +468,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: nil, linkedDiscussionPeer: nil,
members: nil, members: nil,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: nil globalSettings: nil,
invitations: nil
)) ))
case let .user(userPeerId, secretChatId, kind): case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext? let groupsInCommon: GroupsInCommonContext?
@ -603,13 +608,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: nil, linkedDiscussionPeer: nil,
members: nil, members: nil,
encryptionKeyFingerprint: encryptionKeyFingerprint, encryptionKeyFingerprint: encryptionKeyFingerprint,
globalSettings: nil globalSettings: nil,
invitations: nil
) )
} }
case .channel: case .channel:
let status = context.account.viewTracker.peerView(peerId, updateData: false) let status = context.account.viewTracker.peerView(peerId, updateData: false)
|> map { peerView -> PeerInfoStatusData? in |> 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) return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false)
} }
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 { 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])) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
var combinedKeys: [PostboxViewKey] = [] var combinedKeys: [PostboxViewKey] = []
combinedKeys.append(globalNotificationsKey) combinedKeys.append(globalNotificationsKey)
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
return combineLatest( return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true), context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId), peerInfoAvailableMediaPanes(context: context, peerId: peerId),
context.account.postbox.combinedView(keys: combinedKeys), context.account.postbox.combinedView(keys: combinedKeys),
status status,
invitationsContextPromise.get(),
invitationsStatePromise.get()
) )
|> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in |> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations -> PeerInfoScreenData in
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
@ -642,6 +654,18 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
discussionPeer = peer 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( return PeerInfoScreenData(
peer: peerView.peers[peerId], peer: peerView.peers[peerId],
cachedData: peerView.cachedData, cachedData: peerView.cachedData,
@ -654,7 +678,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: discussionPeer, linkedDiscussionPeer: discussionPeer,
members: nil, members: nil,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: nil globalSettings: nil,
invitations: invitations
) )
} }
case let .group(groupId): 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])) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
var combinedKeys: [PostboxViewKey] = [] var combinedKeys: [PostboxViewKey] = []
combinedKeys.append(globalNotificationsKey) combinedKeys.append(globalNotificationsKey)
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
return combineLatest(queue: .mainQueue(), return combineLatest(queue: .mainQueue(),
context.account.viewTracker.peerView(groupId, updateData: true), context.account.viewTracker.peerView(groupId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: groupId), peerInfoAvailableMediaPanes(context: context, peerId: groupId),
context.account.postbox.combinedView(keys: combinedKeys), context.account.postbox.combinedView(keys: combinedKeys),
status, 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 var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { 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( return PeerInfoScreenData(
peer: peerView.peers[groupId], peer: peerView.peers[groupId],
cachedData: peerView.cachedData, cachedData: peerView.cachedData,
@ -792,7 +837,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
linkedDiscussionPeer: discussionPeer, linkedDiscussionPeer: discussionPeer,
members: membersData, members: membersData,
encryptionKeyFingerprint: nil, encryptionKeyFingerprint: nil,
globalSettings: nil globalSettings: nil,
invitations: invitations
) )
} }
} }

View File

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

View File

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

View File

@ -91,8 +91,8 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
} }
let resolvedWallpaper: Signal<TelegramWallpaper?, NoError> let resolvedWallpaper: Signal<TelegramWallpaper?, NoError>
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 { if case let .file(id, _, _, _, _, _, slug, _, settings) = presentationTheme.chat.defaultWallpaper, id == 0 {
resolvedWallpaper = cachedWallpaper(account: account, slug: file.slug, settings: file.settings) resolvedWallpaper = cachedWallpaper(account: account, slug: slug, settings: settings)
|> map { wallpaper in |> map { wallpaper in
return wallpaper?.wallpaper return wallpaper?.wallpaper
} }
@ -102,15 +102,15 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
return resolvedWallpaper return resolvedWallpaper
|> mapToSignal { wallpaper -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in |> 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] = [] 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))) 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.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) 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 |> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in
guard complete, let fullSizeData = fullSizeData else { guard complete, let fullSizeData = fullSizeData else {
return .complete() 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)) return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme))
} }
} else { } else {