mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '2266e38d691b345f49ae3c2cf9cc669c808b31d6'
This commit is contained in:
commit
347cc10a6e
@ -5845,6 +5845,7 @@ Sorry for the inconvenience.";
|
||||
"InviteLink.Expired" = "expired";
|
||||
"InviteLink.UsageLimitReached" = "limit reached";
|
||||
"InviteLink.Revoked" = "revoked";
|
||||
"InviteLink.TapToCopy" = "tap to copy";
|
||||
|
||||
"InviteLink.AdditionalLinks" = "Additional Links";
|
||||
"InviteLink.Create" = "Create a New Link";
|
||||
@ -5885,10 +5886,17 @@ Sorry for the inconvenience.";
|
||||
"InviteLink.InviteLink" = "Invite Link";
|
||||
"InviteLink.CreatedBy" = "Link Created By";
|
||||
|
||||
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links";
|
||||
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete";
|
||||
"InviteLink.DeleteLinkAlert.Text" = "Are you sure you want to delete this link? It will be completely gone.";
|
||||
"InviteLink.DeleteLinkAlert.Action" = "Delete";
|
||||
|
||||
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links.";
|
||||
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete All";
|
||||
|
||||
"InviteLink.ExpiresIn" = "expires in %@";
|
||||
|
||||
"Conversation.ChecksTooltip.Delivered" = "Delivered";
|
||||
"Conversation.ChecksTooltip.Read" = "Read";
|
||||
|
||||
"DialogList.MultipleTypingPair" = "%@ and %@ are typing";
|
||||
|
||||
"Common.Save" = "Save";
|
||||
|
@ -1212,8 +1212,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
strongSelf.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
switch controller.content {
|
||||
case let .archivedChat(archivedChat):
|
||||
if peerIds.contains(PeerId(archivedChat.peerId)) {
|
||||
case let .archivedChat(peerId, _, _, _):
|
||||
if peerIds.contains(PeerId(peerId)) {
|
||||
controller.dismiss()
|
||||
}
|
||||
default:
|
||||
|
@ -158,7 +158,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
|
||||
interaction.activateSearch()
|
||||
})
|
||||
case let .sort(theme, strings, sortOrder):
|
||||
case let .sort(_, strings, sortOrder):
|
||||
var text = strings.Contacts_SortedByName
|
||||
if case .presence = sortOrder {
|
||||
text = strings.Contacts_SortedByPresence
|
||||
@ -166,17 +166,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
|
||||
interaction.openSortMenu()
|
||||
})
|
||||
case let .permissionInfo(theme, title, text, suppressed):
|
||||
case let .permissionInfo(_, title, text, suppressed):
|
||||
return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
|
||||
interaction.suppressWarning()
|
||||
})
|
||||
case let .permissionEnable(theme, text):
|
||||
case let .permissionEnable(_, text):
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: {
|
||||
interaction.authorize()
|
||||
})
|
||||
case let .option(_, option, header, theme, _):
|
||||
case let .option(_, option, header, _, _):
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action)
|
||||
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
|
||||
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
|
||||
var status: ContactsPeerItemStatus
|
||||
let itemPeer: ContactsPeerItemPeer
|
||||
var isContextActionEnabled = false
|
||||
@ -928,9 +928,9 @@ public final class ContactListNode: ASDisplayNode {
|
||||
|> mapToSignal { presentation in
|
||||
var generateSections = false
|
||||
var includeChatList = false
|
||||
if case let .natural(natural) = presentation {
|
||||
if case let .natural(_, includeChatListValue) = presentation {
|
||||
generateSections = true
|
||||
includeChatList = natural.includeChatList
|
||||
includeChatList = includeChatListValue
|
||||
}
|
||||
|
||||
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {
|
||||
|
@ -1225,7 +1225,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
|
||||
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
|
||||
|
||||
if let (media, _) = mediaForMessage(message: message) {
|
||||
if let _ = mediaForMessage(message: message) {
|
||||
centralItemNode.activateAsInitial()
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ func isValidNumberOfUsers(_ number: String) -> Bool {
|
||||
private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
case timeHeader(PresentationTheme, String)
|
||||
case timePicker(PresentationTheme, InviteLinkTimeLimit)
|
||||
case timeExpiryDate(PresentationTheme, Int32?)
|
||||
case timeExpiryDate(PresentationTheme, Int32?, Bool)
|
||||
case timeCustomPicker(PresentationTheme, Int32?)
|
||||
case timeInfo(PresentationTheme, String)
|
||||
|
||||
@ -111,8 +111,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .timeExpiryDate(lhsTheme, lhsDate):
|
||||
if case let .timeExpiryDate(rhsTheme, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate {
|
||||
case let .timeExpiryDate(lhsTheme, lhsDate, lhsActive):
|
||||
if case let .timeExpiryDate(rhsTheme, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate, lhsActive == rhsActive {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -182,14 +182,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
return updatedState
|
||||
})
|
||||
})
|
||||
case let .timeExpiryDate(_, value):
|
||||
case let .timeExpiryDate(theme, value, active):
|
||||
let text: String
|
||||
if let value = value {
|
||||
text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."))
|
||||
} else {
|
||||
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
|
||||
}
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
|
||||
arguments.dismissInput()
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
@ -223,7 +223,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
||||
})
|
||||
case let .usageCustomPicker(theme, value, focused):
|
||||
let text = value.flatMap { String($0) } ?? (focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited)
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, tag: nil, sectionId: self.section, textUpdated: { updatedText in
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, tag: nil, sectionId: self.section, textUpdated: { updatedText in
|
||||
guard !updatedText.isEmpty else {
|
||||
return
|
||||
}
|
||||
@ -276,7 +276,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
|
||||
} else if let value = state.time.value {
|
||||
time = currentTime + value
|
||||
}
|
||||
entries.append(.timeExpiryDate(presentationData.theme, time))
|
||||
entries.append(.timeExpiryDate(presentationData.theme, time, state.pickingTimeLimit))
|
||||
if state.pickingTimeLimit {
|
||||
entries.append(.timeCustomPicker(presentationData.theme, time ?? currentTime))
|
||||
}
|
||||
@ -300,9 +300,10 @@ private struct InviteLinkEditControllerState: Equatable {
|
||||
var time: InviteLinkTimeLimit
|
||||
var pickingTimeLimit = false
|
||||
var pickingUsageLimit = false
|
||||
var updating = false
|
||||
}
|
||||
|
||||
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: (() -> Void)? = nil) -> ViewController {
|
||||
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: ((ExportedInvitation?) -> Void)? = nil) -> ViewController {
|
||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
@ -327,7 +328,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
|
||||
|
||||
initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
} else {
|
||||
initialState = InviteLinkEditControllerState(usage: .medium, time: .week, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
initialState = InviteLinkEditControllerState(usage: .unlimited, time: .unlimited, pickingTimeLimit: false, pickingUsageLimit: false)
|
||||
}
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
@ -359,8 +360,17 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
|
||||
dismissAction()
|
||||
dismissImpl?()
|
||||
|
||||
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
completion?()
|
||||
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link)
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
}, error: { _ in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = false
|
||||
return updatedState
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
})
|
||||
]),
|
||||
@ -369,6 +379,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
|
||||
presentControllerImpl?(controller, nil)
|
||||
})
|
||||
|
||||
let previousState = Atomic<InviteLinkEditControllerState?>(value: nil)
|
||||
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
@ -376,7 +387,13 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
|
||||
dismissImpl?()
|
||||
})
|
||||
|
||||
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
|
||||
let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: {
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = true
|
||||
return updatedState
|
||||
}
|
||||
|
||||
let expireDate: Int32?
|
||||
if case let .custom(value) = state.time {
|
||||
expireDate = value
|
||||
@ -390,21 +407,43 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
|
||||
let usageLimit = state.usage.value
|
||||
if invite == nil {
|
||||
let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
completion?()
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
dismissImpl?()
|
||||
}, error: { _ in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = false
|
||||
return updatedState
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
} else if let invite = invite {
|
||||
let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
completion?()
|
||||
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|
||||
|> deliverOnMainQueue).start(next: { invite in
|
||||
completion?(invite)
|
||||
dismissImpl?()
|
||||
}, error: { _ in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updating = false
|
||||
return updatedState
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let previousState = previousState.swap(state)
|
||||
var animateChanges = false
|
||||
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit {
|
||||
animateChanges = true
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
@ -45,25 +45,39 @@ private struct InviteLinkInviteTransaction {
|
||||
}
|
||||
|
||||
private enum InviteLinkInviteEntryId: Hashable {
|
||||
case header
|
||||
case mainLink
|
||||
case links(Int32)
|
||||
case manage
|
||||
}
|
||||
|
||||
private enum InviteLinkInviteEntry: Comparable, Identifiable {
|
||||
case header(PresentationTheme, String, String)
|
||||
case mainLink(PresentationTheme, ExportedInvitation)
|
||||
case links(Int32, PresentationTheme, [ExportedInvitation])
|
||||
case manage(PresentationTheme, String, Bool)
|
||||
|
||||
var stableId: InviteLinkInviteEntryId {
|
||||
switch self {
|
||||
case .header:
|
||||
return .header
|
||||
case .mainLink:
|
||||
return .mainLink
|
||||
case let .links(index, _, _):
|
||||
return .links(index)
|
||||
case .manage:
|
||||
return .manage
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .header(lhsTheme, lhsTitle, lhsText):
|
||||
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .mainLink(lhsTheme, lhsInvitation):
|
||||
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
|
||||
return true
|
||||
@ -76,43 +90,73 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .manage(lhsTheme, lhsText, lhsStandalone):
|
||||
if case let .manage(rhsTheme, rhsText, rhsStandalone) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStandalone == rhsStandalone {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .header:
|
||||
switch rhs {
|
||||
case .header:
|
||||
return false
|
||||
case .mainLink, .links, .manage:
|
||||
return true
|
||||
}
|
||||
case .mainLink:
|
||||
switch rhs {
|
||||
case .mainLink:
|
||||
case .header, .mainLink:
|
||||
return false
|
||||
case .links:
|
||||
case .links, .manage:
|
||||
return true
|
||||
}
|
||||
case let .links(lhsIndex, _, _):
|
||||
switch rhs {
|
||||
case .mainLink:
|
||||
case .header, .mainLink:
|
||||
return false
|
||||
case let .links(rhsIndex, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
case .manage:
|
||||
return true
|
||||
}
|
||||
case .manage:
|
||||
switch rhs {
|
||||
case .header, .mainLink, .links:
|
||||
return false
|
||||
case .manage:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .header(theme, title, text):
|
||||
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
|
||||
case let .mainLink(_, invite):
|
||||
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: nil, sectionId: 0, style: .plain, shareAction: {
|
||||
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
|
||||
interaction.copyLink(invite)
|
||||
}, shareAction: {
|
||||
interaction.shareLink(invite)
|
||||
}, contextAction: { node in
|
||||
interaction.mainLinkContextAction(invite, node, nil)
|
||||
}, viewAction: {
|
||||
})
|
||||
case let .links(_, _, invites):
|
||||
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, sectionId: 0, style: .plain, tapAction: { invite in
|
||||
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, share: true, sectionId: 1, style: .plain, tapAction: { invite in
|
||||
interaction.copyLink(invite)
|
||||
}, contextAction: { invite, _ in
|
||||
interaction.shareLink(invite)
|
||||
})
|
||||
case let .manage(theme, text, standalone):
|
||||
return InviteLinkInviteManageItem(theme: theme, text: text, standalone: standalone, action: {
|
||||
interaction.manageLinks()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,13 +180,14 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private weak var parentNavigationController: NavigationController?
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId) {
|
||||
fatalError()
|
||||
public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.parentNavigationController = parentNavigationController
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -196,7 +241,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.dismiss(animated: false)
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -212,6 +257,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let invitesContext: PeerExportedInvitationsContext
|
||||
|
||||
private var interaction: InviteLinkInviteInteraction?
|
||||
|
||||
@ -244,6 +290,8 @@ public final class InviteLinkInviteController: ViewController {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationDataPromise = Promise(self.presentationData)
|
||||
self.controller = controller
|
||||
|
||||
self.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
@ -328,7 +376,7 @@ public final class InviteLinkInviteController: ViewController {
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
||||
dismissAction()
|
||||
|
||||
self?.revokeDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
|
||||
self?.revokeDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
}))
|
||||
})
|
||||
@ -341,31 +389,53 @@ public final class InviteLinkInviteController: ViewController {
|
||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
self?.controller?.presentInGlobalOverlay(contextController)
|
||||
}, copyLink: { [weak self] invite in
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
self?.controller?.present(shareController, in: .window(.root))
|
||||
UIPasteboard.general.string = invite.link
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
|
||||
}, shareLink: { [weak self] invite in
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
self?.controller?.present(shareController, in: .window(.root))
|
||||
}, manageLinks: { [weak self] in
|
||||
let controller = inviteLinkListController(context: context, peerId: peerId)
|
||||
self?.controller?.push(controller)
|
||||
self?.controller?.parentNavigationController?.pushViewController(controller)
|
||||
self?.controller?.dismiss()
|
||||
})
|
||||
|
||||
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
|
||||
|
||||
let peerView = context.account.postbox.peerView(id: peerId)
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view in
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, self.invitesContext.state)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in
|
||||
if let strongSelf = self {
|
||||
var entries: [InviteLinkInviteEntry] = []
|
||||
|
||||
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
|
||||
|
||||
let mainInvite: ExportedInvitation?
|
||||
if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
|
||||
entries.append(.mainLink(presentationData.theme, invite))
|
||||
mainInvite = invite
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
|
||||
entries.append(.mainLink(presentationData.theme, invite))
|
||||
mainInvite = invite
|
||||
} else {
|
||||
mainInvite = nil
|
||||
}
|
||||
if let mainInvite = mainInvite {
|
||||
entries.append(.mainLink(presentationData.theme, mainInvite))
|
||||
}
|
||||
|
||||
let additionalInvites = invites.invitations.filter { $0.link != mainInvite?.link }
|
||||
var index: Int32 = 0
|
||||
for i in stride(from: 0, to: additionalInvites.endIndex, by: 2) {
|
||||
var invitesPair: [ExportedInvitation] = []
|
||||
invitesPair.append(additionalInvites[i])
|
||||
if i + 1 < additionalInvites.count {
|
||||
invitesPair.append(additionalInvites[i + 1])
|
||||
}
|
||||
entries.append(.links(index, presentationData.theme, invitesPair))
|
||||
index += 1
|
||||
}
|
||||
|
||||
entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage, additionalInvites.isEmpty))
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
|
||||
@ -507,14 +577,14 @@ public final class InviteLinkInviteController: ViewController {
|
||||
insets.bottom = layout.intrinsicInsets.bottom
|
||||
|
||||
let headerHeight: CGFloat = 54.0
|
||||
let visibleItemsHeight: CGFloat = 147.0 + floor(52.0 * 3.5)
|
||||
let visibleItemsHeight: CGFloat = 409.0
|
||||
|
||||
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
|
||||
|
||||
let listTopInset = layoutTopInset + headerHeight
|
||||
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
|
||||
|
||||
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight)
|
||||
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight - insets.bottom)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve)
|
||||
@ -539,11 +609,6 @@ public final class InviteLinkInviteController: ViewController {
|
||||
if result === self.headerNode.view {
|
||||
return self.view
|
||||
}
|
||||
|
||||
if result === self.headerNode.view {
|
||||
return self.view
|
||||
}
|
||||
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
@ -569,8 +634,6 @@ public final class InviteLinkInviteController: ViewController {
|
||||
case .changed:
|
||||
var translation = recognizer.translation(in: self.contentNode.view).y
|
||||
if let currentPanOffset = self.panGestureArguments {
|
||||
|
||||
|
||||
if case let .known(value) = contentOffset, value <= 0.5 {
|
||||
} else {
|
||||
translation = currentPanOffset
|
||||
|
@ -13,10 +13,12 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
|
||||
var sectionId: ItemListSectionId = 0
|
||||
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
init(theme: PresentationTheme, text: String) {
|
||||
init(theme: PresentationTheme, title: String, text: String) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.text = text
|
||||
}
|
||||
|
||||
@ -57,58 +59,86 @@ class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(13.0)
|
||||
private let titleFont = Font.medium(23.0)
|
||||
private let textFont = Font.regular(13.0)
|
||||
|
||||
class InviteLinkInviteHeaderItemNode: ListViewItemNode {
|
||||
private let titleNode: TextNode
|
||||
private var animationNode: AnimatedStickerNode
|
||||
private let textNode: TextNode
|
||||
private let iconBackgroundNode: ASImageNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
private var item: InviteLinkInviteHeaderItem?
|
||||
|
||||
init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconBackgroundNode = ASImageNode()
|
||||
self.iconBackgroundNode.displaysAsynchronously = false
|
||||
self.iconBackgroundNode.displayWithoutProcessing = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.contentMode = .center
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.iconBackgroundNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 32.0 + params.leftInset
|
||||
let topInset: CGFloat = 92.0
|
||||
let leftInset: CGFloat = 40.0 + params.leftInset
|
||||
let topInset: CGFloat = 98.0
|
||||
let spacing: CGFloat = 8.0
|
||||
let bottomInset: CGFloat = 24.0
|
||||
|
||||
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + spacing + textLayout.size.height + bottomInset)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.accessibilityLabel = attributedText.string
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 92.0, color: item.theme.actionSheet.controlAccentColor)
|
||||
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/LargeLink"), color: item.theme.list.itemCheckColors.foregroundColor)
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 96.0, height: 96.0)
|
||||
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
|
||||
strongSelf.animationNode.updateLayout(size: iconSize)
|
||||
let iconSize = CGSize(width: 92.0, height: 92.0)
|
||||
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
|
||||
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
|
||||
|
||||
let _ = titleApply()
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
|
||||
|
||||
let _ = textApply()
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 8.0 + titleLayout.size.height + spacing), size: textLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -22,18 +22,20 @@ import ShareController
|
||||
|
||||
private final class InviteLinkListControllerArguments {
|
||||
let context: AccountContext
|
||||
let shareMainLink: (ExportedInvitation?) -> Void
|
||||
let openMainLink: (ExportedInvitation?) -> Void
|
||||
let shareMainLink: (ExportedInvitation) -> Void
|
||||
let openMainLink: (ExportedInvitation) -> Void
|
||||
let copyLink: (ExportedInvitation) -> Void
|
||||
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
|
||||
let createLink: () -> Void
|
||||
let openLink: (ExportedInvitation) -> Void
|
||||
let linkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
|
||||
let deleteAllRevokedLinks: () -> Void
|
||||
|
||||
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation?) -> Void, openMainLink: @escaping (ExportedInvitation?) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
|
||||
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation) -> Void, openMainLink: @escaping (ExportedInvitation) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.shareMainLink = shareMainLink
|
||||
self.openMainLink = openMainLink
|
||||
self.copyLink = copyLink
|
||||
self.mainLinkContextAction = mainLinkContextAction
|
||||
self.createLink = createLink
|
||||
self.openLink = openLink
|
||||
@ -178,8 +180,14 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
case let .mainLinkHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .mainLink(_, invite, peers):
|
||||
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
|
||||
arguments.shareMainLink(invite)
|
||||
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, displayButton: true, displayImporters: true, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
|
||||
if let invite = invite {
|
||||
arguments.copyLink(invite)
|
||||
}
|
||||
}, shareAction: {
|
||||
if let invite = invite {
|
||||
arguments.shareMainLink(invite)
|
||||
}
|
||||
}, contextAction: { node in
|
||||
arguments.mainLinkContextAction(invite, node, nil)
|
||||
}, viewAction: {
|
||||
@ -194,7 +202,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
arguments.createLink()
|
||||
})
|
||||
case let .links(_, _, invites):
|
||||
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in
|
||||
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
|
||||
arguments.openLink(invite)
|
||||
}, contextAction: { invite, node in
|
||||
arguments.linkContextAction(invite, node, nil)
|
||||
@ -208,7 +216,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
arguments.deleteAllRevokedLinks()
|
||||
})
|
||||
case let .revokedLinks(_, _, invites):
|
||||
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in
|
||||
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
|
||||
arguments.openLink(invite)
|
||||
}, contextAction: { invite, node in
|
||||
arguments.linkContextAction(invite, node, nil)
|
||||
@ -298,34 +306,22 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
|
||||
let deleteAllRevokedLinksDisposable = MetaDisposable()
|
||||
actionsDisposable.add(deleteAllRevokedLinksDisposable)
|
||||
|
||||
actionsDisposable.add((context.account.viewTracker.peerView(peerId) |> filter { $0.cachedData != nil } |> take(1) |> mapToSignal { view -> Signal<String?, NoError> in
|
||||
return ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId)
|
||||
|> mapToSignal { _ -> Signal<String?, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
|
||||
|
||||
var getControllerImpl: (() -> ViewController?)?
|
||||
|
||||
let invitesPromise = Promise<ExportedInvitations?>()
|
||||
invitesPromise.set(.single(nil)
|
||||
|> then(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false)))
|
||||
|
||||
let revokedInvitesPromise = Promise<ExportedInvitations?>()
|
||||
revokedInvitesPromise.set(.single(nil)
|
||||
|> then(peerExportedInvitations(account: context.account, peerId: peerId, revoked: true)))
|
||||
let invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
|
||||
let revokedInvitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: true, forceUpdate: true)
|
||||
|
||||
let arguments = InviteLinkListControllerArguments(context: context, shareMainLink: { invite in
|
||||
if let invite = invite {
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}, openMainLink: { invite in
|
||||
if let invite = invite {
|
||||
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil)
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
|
||||
pushControllerImpl?(controller)
|
||||
}, copyLink: { invite in
|
||||
UIPasteboard.general.string = invite.link
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
|
||||
}, mainLinkContextAction: { invite, node, gesture in
|
||||
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
|
||||
return
|
||||
@ -383,13 +379,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
}
|
||||
}
|
||||
if revoke {
|
||||
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
|
||||
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.revokingPrivateLink = false
|
||||
return updatedState
|
||||
}
|
||||
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
|
||||
|
||||
invitesContext.reload()
|
||||
revokedInvitesContext.reload()
|
||||
}))
|
||||
}
|
||||
})
|
||||
@ -402,14 +400,16 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
presentInGlobalOverlayImpl?(contextController)
|
||||
}, createLink: {
|
||||
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: {
|
||||
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
|
||||
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: { invite in
|
||||
if let invite = invite {
|
||||
invitesContext.add(invite)
|
||||
}
|
||||
})
|
||||
controller.navigationPresentation = .modal
|
||||
pushControllerImpl?(controller)
|
||||
}, openLink: { invite in
|
||||
if let invite = invite {
|
||||
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil)
|
||||
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: invitesContext, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
}, linkContextAction: { invite, node, gesture in
|
||||
@ -445,8 +445,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: {
|
||||
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
|
||||
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: { invite in
|
||||
if let invite = invite {
|
||||
if invite.isRevoked {
|
||||
invitesContext.remove(invite)
|
||||
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
|
||||
} else {
|
||||
invitesContext.update(invite)
|
||||
}
|
||||
}
|
||||
})
|
||||
controller.navigationPresentation = .modal
|
||||
pushControllerImpl?(controller)
|
||||
@ -465,13 +472,15 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
}
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
|
||||
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
|
||||
ActionSheetTextItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Text),
|
||||
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
|
||||
dismissAction()
|
||||
|
||||
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
revokeLinkDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
}))
|
||||
|
||||
revokedInvitesContext.remove(invite)
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
@ -497,6 +506,9 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
}))
|
||||
|
||||
invitesContext.remove(invite)
|
||||
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
@ -520,8 +532,9 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
dismissAction()
|
||||
|
||||
deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
|
||||
|
||||
}))
|
||||
|
||||
revokedInvitesContext.clear()
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
@ -551,11 +564,11 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
|
||||
importersState.set(context.state |> map(Optional.init))
|
||||
}
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesPromise.get(), revokedInvitesPromise.get())
|
||||
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.InviteLink_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites?.list, revokedInvites: revokedInvites?.list, mainPeers: importers?.importers.compactMap { $0.peer.peer } ?? []), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites.invitations, revokedInvites: revokedInvites.invitations, mainPeers: importers?.importers.compactMap { $0.peer.peer } ?? []), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ public final class InviteLinkQRCodeController: ViewController {
|
||||
self.contentContainerNode.addSubnode(self.qrIconNode)
|
||||
self.contentContainerNode.addSubnode(self.qrButtonNode)
|
||||
|
||||
let textFont = Font.regular(16.0)
|
||||
let textFont = Font.regular(13.0)
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Info, font: textFont, textColor: secondaryTextColor)
|
||||
self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share
|
||||
@ -342,7 +342,6 @@ public final class InviteLinkQRCodeController: ViewController {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
||||
let makeImageLayout = self.qrImageNode.asyncLayout()
|
||||
@ -352,7 +351,7 @@ public final class InviteLinkQRCodeController: ViewController {
|
||||
|
||||
let _ = imageApply()
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 24.0), size: imageSize)
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 16.0), size: imageSize)
|
||||
transition.updateFrame(node: self.qrImageNode, frame: imageFrame)
|
||||
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
|
||||
|
||||
@ -365,7 +364,7 @@ public final class InviteLinkQRCodeController: ViewController {
|
||||
|
||||
let inset: CGFloat = 22.0
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxX + 20.0), size: textSize)
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxY + 20.0), size: textSize)
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
|
||||
let buttonSideInset: CGFloat = 16.0
|
||||
@ -379,7 +378,7 @@ public final class InviteLinkQRCodeController: ViewController {
|
||||
|
||||
|
||||
let titleHeight: CGFloat = 54.0
|
||||
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 52.0 + 77.0
|
||||
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 121.0
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
@ -401,7 +400,7 @@ public final class InviteLinkQRCodeController: ViewController {
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 16.0), size: cancelSize)
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 18.0), size: cancelSize)
|
||||
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
|
||||
|
||||
let buttonInset: CGFloat = 16.0
|
||||
|
@ -24,12 +24,14 @@ import DirectionalPanGesture
|
||||
class InviteLinkViewInteraction {
|
||||
let context: AccountContext
|
||||
let openPeer: (PeerId) -> Void
|
||||
let copyLink: (ExportedInvitation) -> Void
|
||||
let shareLink: (ExportedInvitation) -> Void
|
||||
let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void
|
||||
|
||||
init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) {
|
||||
init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) {
|
||||
self.context = context
|
||||
self.openPeer = openPeer
|
||||
self.copyLink = copyLink
|
||||
self.shareLink = shareLink
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
@ -72,7 +74,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
case creatorHeader(PresentationTheme, String)
|
||||
case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
|
||||
case importerHeader(PresentationTheme, String)
|
||||
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
|
||||
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32, Bool)
|
||||
|
||||
var stableId: InviteLinkViewEntryId {
|
||||
switch self {
|
||||
@ -84,7 +86,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
return .creator
|
||||
case .importerHeader:
|
||||
return .importerHeader
|
||||
case let .importer(_, _, _, peer, _):
|
||||
case let .importer(_, _, _, peer, _, _):
|
||||
return .importer(peer.id)
|
||||
}
|
||||
}
|
||||
@ -115,8 +117,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate):
|
||||
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
|
||||
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading):
|
||||
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsLoading == rhsLoading {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -154,11 +156,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
case .creator, .importer:
|
||||
return true
|
||||
}
|
||||
case let .importer(lhsIndex, _, _, _, _):
|
||||
case let .importer(lhsIndex, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .link, .creatorHeader, .creator, .importerHeader:
|
||||
return false
|
||||
case let .importer(rhsIndex, _, _, _, _):
|
||||
case let .importer(rhsIndex, _, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
@ -168,7 +170,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
switch self {
|
||||
case let .link(_, invite):
|
||||
let buttonColor = color(for: invite)
|
||||
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: buttonColor, sectionId: 0, style: .plain, shareAction: {
|
||||
let availability = invitationAvailability(invite)
|
||||
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: !invite.isRevoked && !availability.isZero, displayImporters: false, buttonColor: buttonColor, sectionId: 0, style: .plain, copyAction: {
|
||||
interaction.copyLink(invite)
|
||||
}, shareAction: {
|
||||
interaction.shareLink(invite)
|
||||
}, contextAction: { node in
|
||||
interaction.contextAction(invite, node, nil)
|
||||
@ -183,11 +188,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
|
||||
case let .importerHeader(_, title):
|
||||
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
|
||||
case let .importer(_, _, dateTimeFormat, peer, date):
|
||||
case let .importer(_, _, dateTimeFormat, peer, date, loading):
|
||||
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
|
||||
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
|
||||
interaction.openPeer(peer.id)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -212,14 +217,18 @@ public final class InviteLinkViewController: ViewController {
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let invite: ExportedInvitation
|
||||
private let invitationsContext: PeerExportedInvitationsContext?
|
||||
private let revokedInvitationsContext: PeerExportedInvitationsContext?
|
||||
private let importersContext: PeerInvitationImportersContext?
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?) {
|
||||
public init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.invite = invite
|
||||
self.invitationsContext = invitationsContext
|
||||
self.revokedInvitationsContext = revokedInvitationsContext
|
||||
self.importersContext = importersContext
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
@ -292,12 +301,16 @@ public final class InviteLinkViewController: ViewController {
|
||||
private let peerId: PeerId
|
||||
private let invite: ExportedInvitation
|
||||
|
||||
private let importersContext: PeerInvitationImportersContext
|
||||
|
||||
private var interaction: InviteLinkViewInteraction?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let presentationDataPromise: Promise<PresentationData>
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private var disposable: Disposable?
|
||||
private let actionDisposable = MetaDisposable()
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let contentNode: ASDisplayNode
|
||||
@ -316,10 +329,6 @@ public final class InviteLinkViewController: ViewController {
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private let importersContext: PeerInvitationImportersContext
|
||||
|
||||
init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?, controller: InviteLinkViewController) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
@ -382,6 +391,10 @@ public final class InviteLinkViewController: ViewController {
|
||||
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always))
|
||||
}
|
||||
}, copyLink: { [weak self] invite in
|
||||
UIPasteboard.general.string = invite.link
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
|
||||
}, shareLink: { [weak self] invite in
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
self?.controller?.present(shareController, in: .window(.root))
|
||||
@ -403,7 +416,33 @@ public final class InviteLinkViewController: ViewController {
|
||||
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
|
||||
})))
|
||||
|
||||
if !invite.isRevoked {
|
||||
if invite.isRevoked {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextDelete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let controller = ActionSheetController(presentationData: presentationData)
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Text),
|
||||
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
|
||||
dismissAction()
|
||||
|
||||
self?.actionDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
|
||||
}))
|
||||
|
||||
self?.controller?.revokedInvitationsContext?.remove(invite)
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
})))
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
@ -430,16 +469,23 @@ public final class InviteLinkViewController: ViewController {
|
||||
entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
|
||||
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, invite.date))
|
||||
|
||||
if !state.importers.isEmpty {
|
||||
if !state.importers.isEmpty || (state.isLoadingMore && state.count > 0) {
|
||||
entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased()))
|
||||
}
|
||||
|
||||
var index: Int32 = 0
|
||||
for importer in state.importers {
|
||||
if let peer = importer.peer.peer {
|
||||
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date))
|
||||
if state.importers.isEmpty && state.isLoadingMore {
|
||||
let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
|
||||
for i in 0 ..< min(4, state.count) {
|
||||
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, fakeUser, 0, true))
|
||||
}
|
||||
} else {
|
||||
for importer in state.importers {
|
||||
if let peer = importer.peer.peer {
|
||||
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date, false))
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
@ -514,8 +560,17 @@ public final class InviteLinkViewController: ViewController {
|
||||
let navigationController = self.controller?.navigationController as? NavigationController
|
||||
self.controller?.dismiss()
|
||||
|
||||
let invitationsContext = self.controller?.invitationsContext
|
||||
if let navigationController = navigationController {
|
||||
let controller = inviteLinkEditController(context: self.context, peerId: self.peerId, invite: self.invite)
|
||||
let controller = inviteLinkEditController(context: self.context, peerId: self.peerId, invite: self.invite, completion: { [weak self] invite in
|
||||
if let invite = invite {
|
||||
if invite.isRevoked {
|
||||
invitationsContext?.remove(invite)
|
||||
} else {
|
||||
invitationsContext?.update(invite)
|
||||
}
|
||||
}
|
||||
})
|
||||
controller.navigationPresentation = .modal
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
@ -638,11 +693,6 @@ public final class InviteLinkViewController: ViewController {
|
||||
if result === self.headerNode.view {
|
||||
return self.view
|
||||
}
|
||||
|
||||
if result === self.headerNode.view {
|
||||
return self.view
|
||||
}
|
||||
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
@ -42,10 +42,60 @@ func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
|
||||
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
|
||||
availability = min(fraction, availability)
|
||||
}
|
||||
return availability
|
||||
return max(0.0, min(1.0, availability))
|
||||
}
|
||||
|
||||
private enum ItemBackgroundColor: Equatable {
|
||||
case blue
|
||||
case green
|
||||
case yellow
|
||||
case red
|
||||
case gray
|
||||
|
||||
var colors: (top: UIColor, bottom: UIColor, text: UIColor) {
|
||||
switch self {
|
||||
case .blue:
|
||||
return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff))
|
||||
case .green:
|
||||
return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6))
|
||||
case .yellow:
|
||||
return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7))
|
||||
case .red:
|
||||
return (UIColor(rgb: 0xf2656a), UIColor(rgb: 0xf25f65), UIColor(rgb: 0xffd3de))
|
||||
case .gray:
|
||||
return (UIColor(rgb: 0xa8b2bb), UIColor(rgb: 0xa2abb4), UIColor(rgb: 0xe3e6e8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let moreIcon = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
|
||||
})
|
||||
|
||||
private let shareIcon = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let maskImage = UIImage(bundleImageName: "Chat/Links/Share") {
|
||||
context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - maskImage.size.width) / 2.0), y: floorToScreenPixels((size.height - maskImage.size.height) / 2.0)), size: maskImage.size), mask: maskImage.cgImage!)
|
||||
context.setBlendMode(.clear)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
})
|
||||
|
||||
private class ItemNode: ASDisplayNode {
|
||||
private let selectionNode: HighlightTrackingButtonNode
|
||||
private let wrapperNode: ASDisplayNode
|
||||
private let backgroundNode: ASImageNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
@ -59,19 +109,28 @@ private class ItemNode: ASDisplayNode {
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let subtitleNode: ImmediateTextNode
|
||||
|
||||
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData)?
|
||||
private var updateTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, color: ItemBackgroundColor, presentationData: ItemListPresentationData)?
|
||||
|
||||
var action: (() -> Void)?
|
||||
var contextAction: ((ASDisplayNode) -> Void)?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override init() {
|
||||
self.selectionNode = HighlightTrackingButtonNode()
|
||||
self.wrapperNode = ASDisplayNode()
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
@ -80,37 +139,44 @@ private class ItemNode: ASDisplayNode {
|
||||
self.buttonIconNode = ASImageNode()
|
||||
self.buttonIconNode.displaysAsynchronously = false
|
||||
self.buttonIconNode.displayWithoutProcessing = true
|
||||
self.buttonIconNode.image = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
|
||||
})
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.subtitleNode = ImmediateTextNode()
|
||||
self.subtitleNode.maximumNumberOfLines = 1
|
||||
self.subtitleNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.addSubnode(self.wrapperNode)
|
||||
self.wrapperNode.addSubnode(self.backgroundNode)
|
||||
self.wrapperNode.addSubnode(self.iconNode)
|
||||
|
||||
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||
self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.buttonNode.addSubnode(self.containerNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
self.wrapperNode.addSubnode(self.selectionNode)
|
||||
self.wrapperNode.addSubnode(self.buttonNode)
|
||||
|
||||
self.wrapperNode.addSubnode(self.titleNode)
|
||||
self.wrapperNode.addSubnode(self.subtitleNode)
|
||||
|
||||
self.selectionNode.addTarget(self, action: #selector(self.tapped), forControlEvents: .touchUpInside)
|
||||
self.selectionNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear)
|
||||
transition.updateSublayerTransformScale(node: strongSelf, scale: 0.95)
|
||||
} else {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .linear)
|
||||
transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
@ -126,11 +192,12 @@ private class ItemNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
deinit {
|
||||
self.updateTimer?.invalidate()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
@objc private func tapped() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.action?()
|
||||
}
|
||||
|
||||
@ -138,35 +205,66 @@ private class ItemNode: ASDisplayNode {
|
||||
self.contextAction?(self.extractedContainerNode)
|
||||
}
|
||||
|
||||
func update(size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let updated = self.params?.size != size || self.params?.wide != wide || self.params?.invite != invite
|
||||
self.params = (size, wide, invite, presentationData)
|
||||
|
||||
func update(size: CGSize, wide: Bool, share: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
|
||||
let availability = invitationAvailability(invite)
|
||||
|
||||
var isExpired = false
|
||||
let secondaryTextColor: UIColor
|
||||
let color: ItemBackgroundColor
|
||||
if invite.isRevoked {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xd4d8db).cgColor, UIColor(rgb: 0xced2d5).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xf8f9f9)
|
||||
color = .gray
|
||||
} else if invite.expireDate == nil && invite.usageLimit == nil {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x00b5f7).cgColor, UIColor(rgb: 0x00b2f6).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xa7f4ff)
|
||||
color = .blue
|
||||
} else if availability >= 0.5 {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x4aca62).cgColor, UIColor(rgb: 0x43c85c).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xc5ffe6)
|
||||
color = .green
|
||||
} else if availability > 0.0 {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf8a953).cgColor, UIColor(rgb: 0xf7a64e).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xfeffd7)
|
||||
color = .yellow
|
||||
} else {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf2656a).cgColor, UIColor(rgb: 0xf25f65).cgColor])
|
||||
secondaryTextColor = UIColor(rgb: 0xffd3de)
|
||||
isExpired = true
|
||||
color = .red
|
||||
}
|
||||
|
||||
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0)
|
||||
let previousParams = self.params
|
||||
self.params = (size, wide, invite, color, presentationData)
|
||||
|
||||
let previousExpireDate = previousParams?.invite.expireDate
|
||||
if previousExpireDate != invite.expireDate {
|
||||
self.updateTimer?.invalidate()
|
||||
self.updateTimer = nil
|
||||
|
||||
if let _ = invite.expireDate, availability > 0.0 {
|
||||
let updateTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let (size, wide, invite, _, presentationData) = strongSelf.params {
|
||||
let _ = strongSelf.update(size: size, wide: wide, share: share, invite: invite, presentationData: presentationData, transition: .animated(duration: 0.3, curve: .linear))
|
||||
}
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.updateTimer = updateTimer
|
||||
updateTimer.start()
|
||||
}
|
||||
} else if availability.isZero {
|
||||
self.updateTimer?.invalidate()
|
||||
self.updateTimer = nil
|
||||
}
|
||||
|
||||
let colors: NSArray = [color.colors.top.cgColor, color.colors.bottom.cgColor]
|
||||
if let (_, _, previousInvite, previousColor, _) = previousParams, previousInvite == invite {
|
||||
if previousColor != color {
|
||||
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
|
||||
snapshotView.frame = self.wrapperNode.bounds
|
||||
self.wrapperNode.view.addSubview(snapshotView)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: colors)
|
||||
}
|
||||
} else {
|
||||
self.backgroundNode.image = generateBackgroundImage(colors: colors)
|
||||
}
|
||||
|
||||
let secondaryTextColor = color.colors.text
|
||||
|
||||
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0)
|
||||
var inviteLink = invite.link.replacingOccurrences(of: "https://", with: "")
|
||||
if !wide {
|
||||
inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n")
|
||||
@ -180,11 +278,13 @@ private class ItemNode: ASDisplayNode {
|
||||
}
|
||||
self.titleNode.attributedText = title
|
||||
|
||||
self.buttonIconNode.image = share ? shareIcon : moreIcon
|
||||
|
||||
var subtitleText: String = ""
|
||||
if let count = invite.count {
|
||||
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
|
||||
} else {
|
||||
subtitleText = isExpired || invite.isRevoked ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone
|
||||
subtitleText = [.red, .gray].contains(color) ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone
|
||||
}
|
||||
if invite.isRevoked {
|
||||
if !subtitleText.isEmpty {
|
||||
@ -194,11 +294,27 @@ private class ItemNode: ASDisplayNode {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
|
||||
self.timerNode?.removeFromSupernode()
|
||||
self.timerNode = nil
|
||||
} else if let expireDate = invite.expireDate, currentTime > expireDate {
|
||||
} else if let expireDate = invite.expireDate, currentTime >= expireDate {
|
||||
if !subtitleText.isEmpty {
|
||||
subtitleText += " • "
|
||||
}
|
||||
subtitleText += presentationData.strings.InviteLink_Expired
|
||||
if share {
|
||||
subtitleText = presentationData.strings.InviteLink_Expired
|
||||
} else {
|
||||
subtitleText += presentationData.strings.InviteLink_Expired
|
||||
}
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
|
||||
self.timerNode?.removeFromSupernode()
|
||||
self.timerNode = nil
|
||||
} else if let usageLimit = invite.usageLimit, let count = invite.count, count >= usageLimit {
|
||||
if !subtitleText.isEmpty {
|
||||
subtitleText += " • "
|
||||
}
|
||||
if share {
|
||||
subtitleText = presentationData.strings.InviteLink_UsageLimitReached
|
||||
} else {
|
||||
subtitleText += presentationData.strings.InviteLink_UsageLimitReached
|
||||
}
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
|
||||
self.timerNode?.removeFromSupernode()
|
||||
self.timerNode = nil
|
||||
@ -209,21 +325,27 @@ private class ItemNode: ASDisplayNode {
|
||||
timerNode = current
|
||||
} else {
|
||||
timerNode = TimerNode()
|
||||
timerNode.isUserInteractionEnabled = false
|
||||
self.timerNode = timerNode
|
||||
self.addSubnode(timerNode)
|
||||
}
|
||||
timerNode.update(color: UIColor.white, creationTimestamp: invite.date, deadlineTimestamp: expireDate)
|
||||
if share {
|
||||
subtitleText = presentationData.strings.InviteLink_TapToCopy
|
||||
}
|
||||
} else {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)
|
||||
self.timerNode?.removeFromSupernode()
|
||||
self.timerNode = nil
|
||||
if share {
|
||||
subtitleText = presentationData.strings.InviteLink_TapToCopy
|
||||
}
|
||||
}
|
||||
|
||||
self.iconNode.frame = CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0)
|
||||
self.timerNode?.frame = CGRect(x: 8.0, y: 8.0, width: 34.0, height: 34.0)
|
||||
|
||||
let subtitle: NSMutableAttributedString = NSMutableAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
|
||||
self.subtitleNode.attributedText = subtitle
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
|
||||
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
|
||||
@ -234,7 +356,9 @@ private class ItemNode: ASDisplayNode {
|
||||
let itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: itemSize)
|
||||
transition.updateFrame(node: self.wrapperNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.selectionNode, frame: backgroundFrame)
|
||||
|
||||
let buttonSize = CGSize(width: 26.0, height: 26.0)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize)
|
||||
@ -260,7 +384,7 @@ class InviteLinksGridNode: ASDisplayNode {
|
||||
return result
|
||||
}
|
||||
|
||||
func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], share: Bool, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
self.items = items
|
||||
|
||||
var contentSize: CGSize = size
|
||||
@ -288,7 +412,7 @@ class InviteLinksGridNode: ASDisplayNode {
|
||||
let col = CGFloat(i % 2)
|
||||
let row = floor(CGFloat(i) / 2.0)
|
||||
let wide = (i == self.items.count - 1 && (self.items.count % 2) != 0)
|
||||
let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, invite: invite, presentationData: presentationData, transition: transition)
|
||||
let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, share: share, invite: invite, presentationData: presentationData, transition: transition)
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 4.0 + row * (122.0 + itemSpacing)), size: itemSize)
|
||||
if !wide && col > 0 {
|
||||
itemFrame.origin.x += itemSpacing + itemSize.width
|
||||
@ -408,7 +532,7 @@ private final class TimerNode: ASDisplayNode {
|
||||
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp)
|
||||
fraction = 1.0 - max(0.0, min(0.94, fraction))
|
||||
fraction = 1.0 - max(0.0, min(1.0, fraction))
|
||||
|
||||
let image: UIImage?
|
||||
|
||||
@ -424,44 +548,47 @@ private final class TimerNode: ASDisplayNode {
|
||||
let startAngle: CGFloat = -CGFloat.pi / 2.0
|
||||
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
|
||||
|
||||
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
|
||||
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
|
||||
|
||||
let dt: CGFloat = 1.0 / 60.0
|
||||
var removeIndices: [Int] = []
|
||||
for i in 0 ..< self.particles.count {
|
||||
let currentTime = timestamp - self.particles[i].beginTime
|
||||
if currentTime > self.particles[i].lifetime {
|
||||
removeIndices.append(i)
|
||||
} else {
|
||||
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
|
||||
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
|
||||
self.particles[i].alpha = 1.0 - decelerated
|
||||
|
||||
var p = self.particles[i].position
|
||||
let d = self.particles[i].direction
|
||||
let v = self.particles[i].velocity
|
||||
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
|
||||
self.particles[i].position = p
|
||||
let sparks = fraction > 0.1 && fraction != 1.0
|
||||
if sparks {
|
||||
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
|
||||
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
|
||||
|
||||
let dt: CGFloat = 1.0 / 60.0
|
||||
var removeIndices: [Int] = []
|
||||
for i in 0 ..< self.particles.count {
|
||||
let currentTime = timestamp - self.particles[i].beginTime
|
||||
if currentTime > self.particles[i].lifetime {
|
||||
removeIndices.append(i)
|
||||
} else {
|
||||
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
|
||||
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
|
||||
self.particles[i].alpha = 1.0 - decelerated
|
||||
|
||||
var p = self.particles[i].position
|
||||
let d = self.particles[i].direction
|
||||
let v = self.particles[i].velocity
|
||||
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
|
||||
self.particles[i].position = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in removeIndices.reversed() {
|
||||
self.particles.remove(at: i)
|
||||
}
|
||||
|
||||
let newParticleCount = 1
|
||||
for _ in 0 ..< newParticleCount {
|
||||
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
|
||||
let angle: CGFloat = degrees * CGFloat.pi / 180.0
|
||||
|
||||
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
|
||||
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
|
||||
for i in removeIndices.reversed() {
|
||||
self.particles.remove(at: i)
|
||||
}
|
||||
|
||||
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
|
||||
|
||||
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
|
||||
self.particles.append(particle)
|
||||
let newParticleCount = 1
|
||||
for _ in 0 ..< newParticleCount {
|
||||
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
|
||||
let angle: CGFloat = degrees * CGFloat.pi / 180.0
|
||||
|
||||
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
|
||||
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
|
||||
|
||||
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
|
||||
|
||||
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
|
||||
self.particles.append(particle)
|
||||
}
|
||||
}
|
||||
|
||||
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in
|
||||
@ -476,10 +603,12 @@ private final class TimerNode: ASDisplayNode {
|
||||
context.addPath(path)
|
||||
context.strokePath()
|
||||
|
||||
for particle in self.particles {
|
||||
let size: CGFloat = 2.0
|
||||
context.setAlpha(particle.alpha)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
|
||||
if sparks {
|
||||
for particle in self.particles {
|
||||
let size: CGFloat = 2.0
|
||||
context.setAlpha(particle.alpha)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -73,7 +73,7 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let datePicker: UIDatePicker
|
||||
private var datePicker: UIDatePicker?
|
||||
|
||||
private var item: ItemListDatePickerItem?
|
||||
|
||||
@ -98,25 +98,31 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.datePicker = UIDatePicker()
|
||||
self.datePicker.minimumDate = Date()
|
||||
self.datePicker.datePickerMode = .dateAndTime
|
||||
if #available(iOS 14.0, *) {
|
||||
self.datePicker.preferredDatePickerStyle = .inline
|
||||
}
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addSubview(self.datePicker)
|
||||
let datePicker = UIDatePicker()
|
||||
datePicker.minimumDate = Date()
|
||||
datePicker.datePickerMode = .dateAndTime
|
||||
if #available(iOS 14.0, *) {
|
||||
datePicker.preferredDatePickerStyle = .inline
|
||||
}
|
||||
|
||||
datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
|
||||
|
||||
self.view.addSubview(datePicker)
|
||||
self.datePicker = datePicker
|
||||
}
|
||||
|
||||
@objc private func datePickerUpdated() {
|
||||
self.item?.updated?(Int32(self.datePicker.date.timeIntervalSince1970))
|
||||
guard let datePicker = self.datePicker else {
|
||||
return
|
||||
}
|
||||
self.item?.updated?(Int32(datePicker.date.timeIntervalSince1970))
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
@ -162,8 +168,8 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
}
|
||||
|
||||
strongSelf.datePicker.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
|
||||
strongSelf.datePicker.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
|
||||
strongSelf.datePicker?.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
|
||||
strongSelf.datePicker?.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
|
@ -10,6 +10,7 @@ import ItemListUI
|
||||
public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let invites: [ExportedInvitation]?
|
||||
let share: Bool
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let tapAction: ((ExportedInvitation) -> Void)?
|
||||
@ -19,6 +20,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
|
||||
public init(
|
||||
presentationData: ItemListPresentationData,
|
||||
invites: [ExportedInvitation]?,
|
||||
share: Bool,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle,
|
||||
tapAction: ((ExportedInvitation) -> Void)?,
|
||||
@ -27,6 +29,7 @@ public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.invites = invites
|
||||
self.share = share
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.tapAction = tapAction
|
||||
@ -127,15 +130,21 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
let rightInset = 16.0 + params.rightInset
|
||||
let topInset: CGFloat
|
||||
if case .plain = item.style, case .otherSection = neighbors.top {
|
||||
topInset = 16.0
|
||||
} else {
|
||||
topInset = 4.0
|
||||
}
|
||||
|
||||
|
||||
var height: CGFloat
|
||||
let count = item.invites?.count ?? 0
|
||||
if count > 0 {
|
||||
if count % 2 == 0 {
|
||||
height = 4.0 + 122.0 + 6.0
|
||||
height = topInset + 122.0 + 6.0
|
||||
} else {
|
||||
height = 4.0 + 102.0 + 6.0
|
||||
height = topInset + 102.0 + 6.0
|
||||
}
|
||||
} else {
|
||||
height = 0.001
|
||||
@ -143,9 +152,9 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
itemBackgroundColor = item.presentationData.theme.list.blocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.blocksBackgroundColor
|
||||
insets = UIEdgeInsets()
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
@ -153,7 +162,7 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
|
||||
}
|
||||
if case .sameSection(false) = neighbors.bottom {
|
||||
} else {
|
||||
height += 6.0
|
||||
height += 10.0
|
||||
}
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
|
||||
@ -167,8 +176,8 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
}
|
||||
|
||||
let gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], presentationData: item.presentationData, transition: .immediate)
|
||||
strongSelf.gridNode.frame = CGRect(origin: CGPoint(), size: gridSize)
|
||||
let gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], share: item.share, presentationData: item.presentationData, transition: .immediate)
|
||||
strongSelf.gridNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset - 4.0), size: gridSize)
|
||||
strongSelf.gridNode.action = { invite in
|
||||
item.tapAction?(invite)
|
||||
}
|
||||
@ -178,18 +187,19 @@ public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
if strongSelf.bottomStripeNode.supernode != nil {
|
||||
strongSelf.bottomStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
|
@ -30,9 +30,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let invite: ExportedInvitation?
|
||||
let peers: [Peer]
|
||||
let displayButton: Bool
|
||||
let displayImporters: Bool
|
||||
let buttonColor: UIColor?
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let copyAction: (() -> Void)?
|
||||
let shareAction: (() -> Void)?
|
||||
let contextAction: ((ASDisplayNode) -> Void)?
|
||||
let viewAction: (() -> Void)?
|
||||
@ -43,9 +46,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
presentationData: ItemListPresentationData,
|
||||
invite: ExportedInvitation?,
|
||||
peers: [Peer],
|
||||
displayButton: Bool,
|
||||
displayImporters: Bool,
|
||||
buttonColor: UIColor?,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle,
|
||||
copyAction: (() -> Void)?,
|
||||
shareAction: (() -> Void)?,
|
||||
contextAction: ((ASDisplayNode) -> Void)?,
|
||||
viewAction: (() -> Void)?,
|
||||
@ -55,9 +61,12 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
|
||||
self.presentationData = presentationData
|
||||
self.invite = invite
|
||||
self.peers = peers
|
||||
self.displayButton = displayButton
|
||||
self.displayImporters = displayImporters
|
||||
self.buttonColor = buttonColor
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.copyAction = copyAction
|
||||
self.shareAction = shareAction
|
||||
self.contextAction = contextAction
|
||||
self.viewAction = viewAction
|
||||
@ -108,6 +117,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
private let fieldNode: ASImageNode
|
||||
private let addressNode: TextNode
|
||||
private let fieldButtonNode: HighlightTrackingButtonNode
|
||||
private let extractedContainerNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let addressButtonNode: HighlightTrackingButtonNode
|
||||
@ -151,12 +161,15 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
self.addressNode = TextNode()
|
||||
self.addressNode.isUserInteractionEnabled = false
|
||||
|
||||
self.fieldButtonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.addressButtonNode = HighlightTrackingButtonNode()
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.containerNode.isGestureEnabled = false
|
||||
self.addressButtonIconNode = ASImageNode()
|
||||
self.addressButtonIconNode.contentMode = .center
|
||||
self.addressButtonIconNode.displaysAsynchronously = false
|
||||
self.addressButtonIconNode.displayWithoutProcessing = true
|
||||
|
||||
@ -171,6 +184,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
self.addSubnode(self.fieldNode)
|
||||
self.addSubnode(self.addressNode)
|
||||
self.addSubnode(self.fieldButtonNode)
|
||||
self.addSubnode(self.avatarsNode)
|
||||
self.addSubnode(self.invitedPeersNode)
|
||||
self.addSubnode(self.avatarsButtonNode)
|
||||
@ -183,6 +197,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
self.addSubnode(self.activateArea)
|
||||
|
||||
self.fieldButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.addressNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.addressNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.addressNode.alpha = 1.0
|
||||
strongSelf.addressNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.fieldButtonNode.addTarget(self, action: #selector(self.fieldButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.addressButtonNode.addTarget(self, action: #selector(self.addressButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.addressButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
@ -218,6 +245,12 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
self.avatarsButtonNode.addTarget(self, action: #selector(self.avatarsButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func fieldButtonPressed() {
|
||||
if let item = self.item {
|
||||
item.copyAction?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func addressButtonPressed() {
|
||||
if let item = self.item {
|
||||
item.contextAction?(self.extractedContainerNode)
|
||||
@ -287,7 +320,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
height -= 57.0
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = .clear
|
||||
insets = UIEdgeInsets()
|
||||
@ -296,6 +328,14 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
if !item.displayImporters {
|
||||
height -= 57.0
|
||||
}
|
||||
if !item.displayButton {
|
||||
height -= 63.0
|
||||
}
|
||||
|
||||
contentSize = CGSize(width: params.width, height: height)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
@ -380,10 +420,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight))
|
||||
strongSelf.fieldNode.frame = fieldFrame
|
||||
strongSelf.fieldButtonNode.frame = fieldFrame
|
||||
|
||||
strongSelf.addressNode.frame = CGRect(origin: CGPoint(x: fieldFrame.minX + floorToScreenPixels((fieldFrame.width - addressLayout.size.width) / 2.0), y: fieldFrame.minY + floorToScreenPixels((fieldFrame.height - addressLayout.size.height) / 2.0) + 1.0), size: addressLayout.size)
|
||||
|
||||
strongSelf.addressButtonNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - 38.0, y: verticalInset + 14.0), size: CGSize(width: 24.0, height: 24.0))
|
||||
strongSelf.addressButtonNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - 38.0 - 14.0, y: verticalInset), size: CGSize(width: 52.0, height: 52.0))
|
||||
strongSelf.extractedContainerNode.frame = strongSelf.addressButtonNode.bounds
|
||||
strongSelf.extractedContainerNode.contentRect = strongSelf.addressButtonNode.bounds
|
||||
strongSelf.addressButtonIconNode.frame = strongSelf.addressButtonNode.bounds
|
||||
@ -432,6 +473,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
|
||||
|
||||
strongSelf.avatarsButtonNode.frame = CGRect(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0, width: totalWidth, height: 32.0)
|
||||
strongSelf.avatarsButtonNode.isUserInteractionEnabled = !item.peers.isEmpty
|
||||
|
||||
strongSelf.shareButtonNode?.isHidden = !item.displayButton
|
||||
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
|
||||
strongSelf.avatarsNode.isHidden = !item.displayImporters
|
||||
strongSelf.invitedPeersNode.isHidden = !item.displayImporters
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ public enum ItemListDisclosureStyle {
|
||||
public enum ItemListDisclosureLabelStyle {
|
||||
case text
|
||||
case detailText
|
||||
case coloredText(UIColor)
|
||||
case multilineDetailText
|
||||
case badge(UIColor)
|
||||
case color(UIColor)
|
||||
@ -277,6 +278,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
||||
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
|
||||
labelFont = detailFont
|
||||
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
|
||||
case let .coloredText(color):
|
||||
labelBadgeColor = color
|
||||
labelFont = titleFont
|
||||
default:
|
||||
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
|
||||
labelFont = titleFont
|
||||
|
@ -46,6 +46,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
|
||||
let clearType: ItemListSingleLineInputClearType
|
||||
let maxLength: Int
|
||||
let enabled: Bool
|
||||
let selectAllOnFocus: Bool
|
||||
public let sectionId: ItemListSectionId
|
||||
let action: () -> Void
|
||||
let textUpdated: (String) -> Void
|
||||
@ -55,7 +56,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
|
||||
let cleared: (() -> Void)?
|
||||
public let tag: ItemListItemTag?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
|
||||
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.title = title
|
||||
self.text = text
|
||||
@ -67,6 +68,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
|
||||
self.clearType = clearType
|
||||
self.maxLength = maxLength
|
||||
self.enabled = enabled
|
||||
self.selectAllOnFocus = selectAllOnFocus
|
||||
self.tag = tag
|
||||
self.sectionId = sectionId
|
||||
self.textUpdated = textUpdated
|
||||
@ -494,6 +496,13 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
|
||||
|
||||
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
self.item?.updatedFocus?(true)
|
||||
if self.item?.selectAllOnFocus == true {
|
||||
DispatchQueue.main.async {
|
||||
let startPosition = self.textNode.textField.beginningOfDocument
|
||||
let endPosition = self.textNode.textField.endOfDocument
|
||||
self.textNode.textField.selectedTextRange = self.textNode.textField.textRange(from: startPosition, to: endPosition)
|
||||
}
|
||||
}
|
||||
self.updateClearButtonVisibility()
|
||||
}
|
||||
|
||||
|
@ -368,17 +368,17 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! ChannelAdminControllerArguments
|
||||
switch self {
|
||||
case let .info(theme, strings, dateTimeFormat, peer, presence):
|
||||
case let .info(_, _, dateTimeFormat, peer, presence):
|
||||
return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in
|
||||
}, avatarTapped: {
|
||||
})
|
||||
case let .rankTitle(theme, text, count, limit):
|
||||
case let .rankTitle(_, text, count, limit):
|
||||
var accessoryText: ItemListSectionHeaderAccessoryText?
|
||||
if let count = count {
|
||||
accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic)
|
||||
}
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section)
|
||||
case let .rank(theme, strings, placeholder, text, enabled):
|
||||
case let .rank(_, _, placeholder, text, enabled):
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in
|
||||
arguments.updateRank(text, updatedText)
|
||||
}, shouldUpdateText: { text in
|
||||
@ -392,23 +392,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
|
||||
}, action: {
|
||||
arguments.dismissInput()
|
||||
})
|
||||
case let .rankInfo(theme, text):
|
||||
case let .rankInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .rightsTitle(theme, text):
|
||||
case let .rightsTitle(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .rightItem(theme, _, text, right, flags, value, enabled):
|
||||
case let .rightItem(_, _, text, right, flags, value, enabled):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in
|
||||
arguments.toggleRight(right, flags)
|
||||
}, activatedWhileDisabled: {
|
||||
arguments.toggleRightWhileDisabled(right, flags)
|
||||
})
|
||||
case let .addAdminsInfo(theme, text):
|
||||
case let .addAdminsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .transfer(theme, text):
|
||||
case let .transfer(_, text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.transferOwnership()
|
||||
}, tag: nil)
|
||||
case let .dismiss(theme, text):
|
||||
case let .dismiss(_, text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.dismissAdmin()
|
||||
}, tag: nil)
|
||||
@ -1004,12 +1004,12 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
var currentRank: String?
|
||||
var currentFlags: TelegramChatAdminRightsFlags?
|
||||
switch initialParticipant {
|
||||
case let .creator(creator):
|
||||
currentRank = creator.rank
|
||||
currentFlags = maskRightsFlags
|
||||
case let .member(member):
|
||||
case let .creator(_, adminInfo, rank):
|
||||
currentRank = rank
|
||||
currentFlags = adminInfo?.rights.flags ?? maskRightsFlags.subtracting(.canBeAnonymous)
|
||||
case let .member(_, _, adminInfo, _, rank):
|
||||
if updateFlags == nil {
|
||||
if member.adminInfo?.rights == nil {
|
||||
if adminInfo?.rights == nil {
|
||||
if channel.flags.contains(.isCreator) {
|
||||
updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous])
|
||||
} else if let adminRights = channel.adminRights {
|
||||
@ -1019,8 +1019,8 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
}
|
||||
}
|
||||
}
|
||||
currentRank = member.rank
|
||||
currentFlags = member.adminInfo?.rights.flags
|
||||
currentRank = rank
|
||||
currentFlags = adminInfo?.rights.flags
|
||||
}
|
||||
|
||||
let effectiveRank = updateRank ?? currentRank
|
||||
|
@ -182,17 +182,17 @@ private enum ChannelMembersEntry: ItemListNodeEntry {
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! ChannelMembersControllerArguments
|
||||
switch self {
|
||||
case let .addMember(theme, text):
|
||||
case let .addMember(_, text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.addMember()
|
||||
})
|
||||
case let .inviteLink(theme, text):
|
||||
case let .inviteLink(_, text):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.inviteViaLink()
|
||||
})
|
||||
case let .addMemberInfo(theme, text):
|
||||
case let .addMemberInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled):
|
||||
case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled):
|
||||
let text: ItemListPeerItemText
|
||||
if let user = participant.peer as? TelegramUser, let _ = user.botInfo {
|
||||
text = .text(strings.Bot_GenericBotStatus, .secondary)
|
||||
@ -342,6 +342,8 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
var dismissInputImpl: (() -> Void)?
|
||||
|
||||
var getControllerImpl: (() -> ViewController?)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let addMembersDisposable = MetaDisposable()
|
||||
@ -462,7 +464,10 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
}, inviteViaLink: {
|
||||
pushControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId))
|
||||
if let controller = getControllerImpl?() {
|
||||
dismissInputImpl?()
|
||||
presentControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId, parentNavigationController: controller.navigationController as? NavigationController), nil)
|
||||
}
|
||||
})
|
||||
|
||||
let peerView = context.account.viewTracker.peerView(peerId)
|
||||
@ -551,6 +556,9 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
dismissInputImpl = { [weak controller] in
|
||||
controller?.view.endEditing(true)
|
||||
}
|
||||
getControllerImpl = { [weak controller] in
|
||||
return controller
|
||||
}
|
||||
controller.visibleBottomContentOffsetChanged = { offset in
|
||||
if let loadMoreControl = loadMoreControl, case let .known(value) = offset, value < 40.0 {
|
||||
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl)
|
||||
|
@ -30,11 +30,12 @@ private final class ChannelVisibilityControllerArguments {
|
||||
let displayPrivateLinkMenu: (String) -> Void
|
||||
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||
let revokePeerId: (PeerId) -> Void
|
||||
let shareLink: () -> Void
|
||||
let copyLink: (ExportedInvitation) -> Void
|
||||
let shareLink: (ExportedInvitation) -> Void
|
||||
let linkContextAction: (ASDisplayNode) -> Void
|
||||
let manageInviteLinks: () -> Void
|
||||
|
||||
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, shareLink: @escaping () -> Void, linkContextAction: @escaping (ASDisplayNode) -> Void, manageInviteLinks: @escaping () -> Void) {
|
||||
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode) -> Void, manageInviteLinks: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.updateCurrentType = updateCurrentType
|
||||
self.updatePublicLinkText = updatePublicLinkText
|
||||
@ -42,6 +43,7 @@ private final class ChannelVisibilityControllerArguments {
|
||||
self.displayPrivateLinkMenu = displayPrivateLinkMenu
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
self.revokePeerId = revokePeerId
|
||||
self.copyLink = copyLink
|
||||
self.shareLink = shareLink
|
||||
self.linkContextAction = linkContextAction
|
||||
self.manageInviteLinks = manageInviteLinks
|
||||
@ -77,7 +79,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
||||
case publicLinkAvailability(PresentationTheme, String, Bool)
|
||||
case editablePublicLink(PresentationTheme, PresentationStrings, String, String)
|
||||
case privateLinkHeader(PresentationTheme, String)
|
||||
case privateLink(PresentationTheme, ExportedInvitation?)
|
||||
case privateLink(PresentationTheme, ExportedInvitation?, Bool)
|
||||
case privateLinkInfo(PresentationTheme, String)
|
||||
case privateLinkManage(PresentationTheme, String)
|
||||
case privateLinkManageInfo(PresentationTheme, String)
|
||||
@ -123,18 +125,18 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
||||
return 8
|
||||
case .privateLinkInfo:
|
||||
return 9
|
||||
case .privateLinkManage:
|
||||
return 10
|
||||
case .privateLinkManageInfo:
|
||||
return 11
|
||||
case .publicLinkStatus:
|
||||
return 12
|
||||
return 10
|
||||
case .publicLinkInfo:
|
||||
return 13
|
||||
return 11
|
||||
case .existingLinksInfo:
|
||||
return 14
|
||||
return 12
|
||||
case let .existingLinkPeerItem(index, _, _, _, _, _, _, _):
|
||||
return 15 + index
|
||||
return 13 + index
|
||||
case .privateLinkManage:
|
||||
return 1000
|
||||
case .privateLinkManageInfo:
|
||||
return 1001
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,8 +184,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .privateLink(lhsTheme, lhsInvite):
|
||||
if case let .privateLink(rhsTheme, rhsInvite) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite {
|
||||
case let .privateLink(lhsTheme, lhsInvite, lhsDisplayImporters):
|
||||
if case let .privateLink(rhsTheme, rhsInvite, rhsDisplayImporters) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, lhsDisplayImporters == rhsDisplayImporters {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -290,9 +292,15 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
||||
return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section)
|
||||
case let .privateLinkHeader(_, title):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
||||
case let .privateLink(_, invite):
|
||||
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
|
||||
arguments.shareLink()
|
||||
case let .privateLink(_, invite, displayImporters):
|
||||
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
|
||||
if let invite = invite {
|
||||
arguments.copyLink(invite)
|
||||
}
|
||||
}, shareAction: {
|
||||
if let invite = invite {
|
||||
arguments.shareLink(invite)
|
||||
}
|
||||
}, contextAction: { node in
|
||||
arguments.linkContextAction(node)
|
||||
}, viewAction: {
|
||||
@ -586,15 +594,22 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
|
||||
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_PublicLink_Info))
|
||||
} else {
|
||||
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Channel_Username_CreatePublicLinkHelp))
|
||||
}
|
||||
switch mode {
|
||||
case .initialSetup:
|
||||
break
|
||||
case .generic, .privateLink:
|
||||
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
|
||||
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
|
||||
}
|
||||
}
|
||||
case .privateChannel:
|
||||
let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation
|
||||
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
|
||||
entries.append(.privateLink(presentationData.theme, invite))
|
||||
entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup))
|
||||
if isGroup {
|
||||
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
|
||||
} else {
|
||||
@ -613,7 +628,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
|
||||
case .privateLink:
|
||||
let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation
|
||||
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
|
||||
entries.append(.privateLink(presentationData.theme, invite))
|
||||
entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup))
|
||||
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_Help))
|
||||
switch mode {
|
||||
case .initialSetup:
|
||||
@ -712,7 +727,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
|
||||
case .privateChannel:
|
||||
let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation
|
||||
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
|
||||
entries.append(.privateLink(presentationData.theme, invite))
|
||||
entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup))
|
||||
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
|
||||
switch mode {
|
||||
case .initialSetup:
|
||||
@ -835,14 +850,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
|
||||
let revokeLinkDisposable = MetaDisposable()
|
||||
actionsDisposable.add(revokeLinkDisposable)
|
||||
|
||||
actionsDisposable.add((context.account.viewTracker.peerView(peerId) |> filter { $0.cachedData != nil } |> take(1) |> mapToSignal { view -> Signal<String?, NoError> in
|
||||
return ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId)
|
||||
|> mapToSignal { _ -> Signal<String?, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
}).start())
|
||||
|
||||
|
||||
let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in
|
||||
updateState { state in
|
||||
return state.withUpdatedSelectedType(type)
|
||||
@ -898,22 +906,13 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
}
|
||||
})
|
||||
}))
|
||||
}, shareLink: {
|
||||
let _ = (context.account.postbox.transaction { transaction -> String? in
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
|
||||
if let cachedData = cachedData as? CachedChannelData {
|
||||
return cachedData.exportedInvitation?.link
|
||||
} else if let cachedData = cachedData as? CachedGroupData {
|
||||
return cachedData.exportedInvitation?.link
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} |> deliverOnMainQueue).start(next: { link in
|
||||
if let link = link {
|
||||
let shareController = ShareController(context: context, subject: .url(link))
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}
|
||||
})
|
||||
}, copyLink: { invite in
|
||||
UIPasteboard.general.string = invite.link
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
|
||||
}, shareLink: { invite in
|
||||
let shareController = ShareController(context: context, subject: .url(invite.link))
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}, linkContextAction: { node in
|
||||
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
|
||||
return
|
||||
@ -991,7 +990,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
}
|
||||
}
|
||||
if revoke {
|
||||
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
|
||||
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
|
||||
updateState {
|
||||
$0.withUpdatedRevokingPrivateLink(false)
|
||||
}
|
||||
@ -1066,8 +1065,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
|
||||
updateState { state in
|
||||
return state.withUpdatedUpdatingAddressName(false)
|
||||
}
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
}, completed: {
|
||||
updateState { state in
|
||||
return state.withUpdatedUpdatingAddressName(false)
|
||||
|
@ -3,20 +3,24 @@ import Postbox
|
||||
public final class CachedStickerQueryResult: PostboxCoding {
|
||||
public let items: [TelegramMediaFile]
|
||||
public let hash: Int32
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(items: [TelegramMediaFile], hash: Int32) {
|
||||
public init(items: [TelegramMediaFile], hash: Int32, timestamp: Int32) {
|
||||
self.items = items
|
||||
self.hash = hash
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.items = decoder.decodeObjectArrayForKey("it").map { $0 as! TelegramMediaFile }
|
||||
self.hash = decoder.decodeInt32ForKey("h", orElse: 0)
|
||||
self.timestamp = decoder.decodeInt32ForKey("t", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObjectArray(self.items, forKey: "it")
|
||||
encoder.encodeInt32(self.hash, forKey: "h")
|
||||
encoder.encodeInt32(self.timestamp, forKey: "t")
|
||||
}
|
||||
|
||||
public static func cacheKey(_ query: String) -> ValueBoxKey {
|
||||
|
@ -66,4 +66,8 @@ public struct ExportedInvitation: PostboxCoding, Equatable {
|
||||
public static func ==(lhs: ExportedInvitation, rhs: ExportedInvitation) -> Bool {
|
||||
return lhs.link == rhs.link && lhs.isPermanent == rhs.isPermanent && lhs.isRevoked == rhs.isRevoked && lhs.adminId == rhs.adminId && lhs.date == rhs.date && lhs.startDate == rhs.startDate && lhs.expireDate == rhs.expireDate && lhs.usageLimit == rhs.usageLimit && lhs.count == rhs.count
|
||||
}
|
||||
|
||||
public func withUpdated(isRevoked: Bool) -> ExportedInvitation {
|
||||
return ExportedInvitation(link: self.link, isPermanent: self.isPermanent, isRevoked: isRevoked, adminId: self.adminId, date: self.date, startDate: self.startDate, expireDate: self.expireDate, usageLimit: self.usageLimit, count: self.count)
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ public struct Namespaces {
|
||||
public static let cachedContextResults: Int8 = 10
|
||||
public static let proximityNotificationStoredState: Int8 = 11
|
||||
public static let cachedPeerInvitationImporters: Int8 = 12
|
||||
public static let cachedPeerExportedInvitations: Int8 = 13
|
||||
}
|
||||
|
||||
public struct UnorderedItemList {
|
||||
|
@ -3889,15 +3889,16 @@ public extension Api {
|
||||
})
|
||||
}
|
||||
|
||||
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
|
||||
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetDate: Int32?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1838984707)
|
||||
buffer.appendInt32(1785900140)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 0) != 0 {adminId!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetDate!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
|
||||
serializeInt32(limit, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
|
||||
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetDate", offsetDate), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.messages.ExportedChatInvites?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -1060,25 +1060,7 @@ public final class VoiceChatController: ViewController {
|
||||
if !strongSelf.didSetDataReady {
|
||||
strongSelf.accountPeer = accountPeer
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
|
||||
|
||||
if let peer = peerViewMainPeer(view) {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
let addressName = channel.addressName ?? ""
|
||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
|
||||
if addressName.isEmpty {
|
||||
let _ = ensuredExistingPeerExportedInvitation(account: strongSelf.context.account, peerId: call.peerId).start()
|
||||
}
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
switch group.role {
|
||||
case .creator, .admin:
|
||||
let _ = ensuredExistingPeerExportedInvitation(account: strongSelf.context.account, peerId: call.peerId).start()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
strongSelf.didSetDataReady = true
|
||||
strongSelf.controller?.dataReady.set(true)
|
||||
}
|
||||
|
@ -170,6 +170,8 @@ private var declaredEncodables: Void = {
|
||||
declareEncodable(ValidationMessageAttribute.self, f: { ValidationMessageAttribute(decoder: $0) })
|
||||
declareEncodable(EmojiSearchQueryMessageAttribute.self, f: { EmojiSearchQueryMessageAttribute(decoder: $0) })
|
||||
declareEncodable(CachedPeerInvitationImporters.self, f: { CachedPeerInvitationImporters(decoder: $0) })
|
||||
declareEncodable(CachedPeerExportedInvitations.self, f: { CachedPeerExportedInvitations(decoder: $0) })
|
||||
declareEncodable(ExportedInvitation.self, f: { ExportedInvitation(decoder: $0) })
|
||||
|
||||
return
|
||||
}()
|
||||
|
@ -6,53 +6,45 @@ import MtProtoKit
|
||||
|
||||
import SyncCore
|
||||
|
||||
public func ensuredExistingPeerExportedInvitation(account: Account, peerId: PeerId, revokeExisted: Bool = false) -> Signal<ExportedInvitation?, NoError> {
|
||||
public func revokePersistentPeerExportedInvitation(account: Account, peerId: PeerId) -> Signal<ExportedInvitation?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||
let flags: Int32 = (1 << 2)
|
||||
if let _ = peer as? TelegramChannel {
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, cachedData.exportedInvitation != nil && !revokeExisted {
|
||||
return .single(cachedData.exportedInvitation)
|
||||
} else {
|
||||
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitation? in
|
||||
if let invitation = ExportedInvitation(apiExportedInvite: result) {
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedChannelData {
|
||||
return current.withUpdatedExportedInvitation(invitation)
|
||||
} else {
|
||||
return CachedChannelData().withUpdatedExportedInvitation(invitation)
|
||||
}
|
||||
})
|
||||
return invitation
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitation? in
|
||||
if let invitation = ExportedInvitation(apiExportedInvite: result) {
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedChannelData {
|
||||
return current.withUpdatedExportedInvitation(invitation)
|
||||
} else {
|
||||
return CachedChannelData().withUpdatedExportedInvitation(invitation)
|
||||
}
|
||||
})
|
||||
return invitation
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let _ = peer as? TelegramGroup {
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData, cachedData.exportedInvitation != nil && !revokeExisted {
|
||||
return .complete()
|
||||
} else {
|
||||
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitation? in
|
||||
if let invitation = ExportedInvitation(apiExportedInvite: result) {
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedGroupData {
|
||||
return current.withUpdatedExportedInvitation(invitation)
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
})
|
||||
return invitation
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<ExportedInvitation?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitation? in
|
||||
if let invitation = ExportedInvitation(apiExportedInvite: result) {
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedGroupData {
|
||||
return current.withUpdatedExportedInvitation(invitation)
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
})
|
||||
return invitation
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,8 +57,12 @@ public func ensuredExistingPeerExportedInvitation(account: Account, peerId: Peer
|
||||
} |> switchToLatest
|
||||
}
|
||||
|
||||
public func createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal<ExportedInvitation?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
|
||||
public enum CreatePeerExportedInvitationError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public func createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal<ExportedInvitation?, CreatePeerExportedInvitationError> {
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, CreatePeerExportedInvitationError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||
var flags: Int32 = 0
|
||||
if let _ = expireDate {
|
||||
@ -76,7 +72,7 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit))
|
||||
|> retryRequest
|
||||
|> mapError { _ in return CreatePeerExportedInvitationError.generic }
|
||||
|> map { result -> ExportedInvitation? in
|
||||
if let invitation = ExportedInvitation(apiExportedInvite: result) {
|
||||
return invitation
|
||||
@ -87,59 +83,9 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
} |> switchToLatest
|
||||
}
|
||||
|
||||
public struct ExportedInvitations : Equatable {
|
||||
public let list: [ExportedInvitation]?
|
||||
public let totalCount: Int32
|
||||
}
|
||||
|
||||
public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, offsetLink: String? = nil) -> Signal<ExportedInvitations?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedInvitations?, NoError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||
var flags: Int32 = 0
|
||||
if let _ = offsetLink {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
if revoked {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetLink: offsetLink, limit: 50))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<ExportedInvitations?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitations? in
|
||||
if let result = result, case let .exportedChatInvites(count, apiInvites, users) = result {
|
||||
var peers: [Peer] = []
|
||||
var peersMap: [PeerId: Peer] = [:]
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peersMap[telegramUser.id] = telegramUser
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
|
||||
var invites: [ExportedInvitation] = []
|
||||
for apiInvite in apiInvites {
|
||||
if let invite = ExportedInvitation(apiExportedInvite: apiInvite) {
|
||||
invites.append(invite)
|
||||
}
|
||||
}
|
||||
return ExportedInvitations(list: invites, totalCount: count)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
} |> switchToLatest
|
||||
}
|
||||
|> castError(CreatePeerExportedInvitationError.self)
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
public enum EditPeerExportedInvitationError {
|
||||
@ -218,6 +164,59 @@ public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link:
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
public struct ExportedInvitations : Equatable {
|
||||
public let list: [ExportedInvitation]?
|
||||
public let totalCount: Int32
|
||||
}
|
||||
|
||||
public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, offsetLink: ExportedInvitation? = nil) -> Signal<ExportedInvitations?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedInvitations?, NoError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||
var flags: Int32 = 0
|
||||
if let _ = offsetLink {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
if revoked {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetDate: offsetLink?.date, offsetLink: offsetLink?.link, limit: 50))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<ExportedInvitations?, NoError> in
|
||||
return account.postbox.transaction { transaction -> ExportedInvitations? in
|
||||
if let result = result, case let .exportedChatInvites(count, apiInvites, users) = result {
|
||||
var peers: [Peer] = []
|
||||
var peersMap: [PeerId: Peer] = [:]
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peersMap[telegramUser.id] = telegramUser
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
|
||||
var invites: [ExportedInvitation] = []
|
||||
for apiInvite in apiInvites {
|
||||
if let invite = ExportedInvitation(apiExportedInvite: apiInvite) {
|
||||
invites.append(invite)
|
||||
}
|
||||
}
|
||||
return ExportedInvitations(list: invites, totalCount: count)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
} |> switchToLatest
|
||||
}
|
||||
|
||||
|
||||
public enum DeletePeerExportedInvitationError {
|
||||
case generic
|
||||
}
|
||||
@ -251,6 +250,320 @@ public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: Pe
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
private let cachedPeerExportedInvitationsCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
|
||||
|
||||
public struct PeerExportedInvitationsState: Equatable {
|
||||
public var invitations: [ExportedInvitation]
|
||||
public var isLoadingMore: Bool
|
||||
public var hasLoadedOnce: Bool
|
||||
public var canLoadMore: Bool
|
||||
public var count: Int32
|
||||
}
|
||||
|
||||
final class CachedPeerExportedInvitations: PostboxCoding {
|
||||
let invitations: [ExportedInvitation]
|
||||
let canLoadMore: Bool
|
||||
let count: Int32
|
||||
|
||||
public static func key(peerId: PeerId, revoked: Bool) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8 + 4)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
key.setInt32(8, value: revoked ? 1 : 0)
|
||||
return key
|
||||
}
|
||||
|
||||
init(invitations: [ExportedInvitation], canLoadMore: Bool, count: Int32) {
|
||||
self.invitations = invitations
|
||||
self.canLoadMore = canLoadMore
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.invitations = decoder.decodeObjectArrayForKey("invitations")
|
||||
self.canLoadMore = decoder.decodeBoolForKey("canLoadMore", orElse: false)
|
||||
self.count = decoder.decodeInt32ForKey("count", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObjectArray(self.invitations, forKey: "invitations")
|
||||
encoder.encodeBool(self.canLoadMore, forKey: "canLoadMore")
|
||||
encoder.encodeInt32(self.count, forKey: "count")
|
||||
}
|
||||
}
|
||||
|
||||
private final class PeerExportedInvitationsContextImpl {
|
||||
private let queue: Queue
|
||||
private let account: Account
|
||||
private let peerId: PeerId
|
||||
private let revoked: Bool
|
||||
private var forceUpdate: Bool
|
||||
private let disposable = MetaDisposable()
|
||||
private let updateDisposable = MetaDisposable()
|
||||
private var isLoadingMore: Bool = false
|
||||
private var hasLoadedOnce: Bool = false
|
||||
private var canLoadMore: Bool = true
|
||||
private var loadedFromCache: Bool = false
|
||||
private var results: [ExportedInvitation] = []
|
||||
private var count: Int32
|
||||
private var populateCache: Bool = true
|
||||
|
||||
let state = Promise<PeerExportedInvitationsState>()
|
||||
|
||||
init(queue: Queue, account: Account, peerId: PeerId, revoked: Bool, forceUpdate: Bool) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
self.revoked = revoked
|
||||
self.forceUpdate = forceUpdate
|
||||
|
||||
self.count = 0
|
||||
|
||||
self.isLoadingMore = true
|
||||
self.disposable.set((account.postbox.transaction { transaction -> CachedPeerExportedInvitations? in
|
||||
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerExportedInvitations, key: CachedPeerExportedInvitations.key(peerId: peerId, revoked: revoked))) as? CachedPeerExportedInvitations
|
||||
}
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] cachedResult in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isLoadingMore = false
|
||||
if let cachedResult = cachedResult {
|
||||
strongSelf.results = cachedResult.invitations
|
||||
strongSelf.count = cachedResult.count
|
||||
strongSelf.hasLoadedOnce = true
|
||||
strongSelf.canLoadMore = cachedResult.canLoadMore
|
||||
strongSelf.loadedFromCache = true
|
||||
}
|
||||
strongSelf.loadMore()
|
||||
}))
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.updateDisposable.dispose()
|
||||
}
|
||||
|
||||
func reload() {
|
||||
self.forceUpdate = true
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
func loadMore() {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
self.isLoadingMore = true
|
||||
let account = self.account
|
||||
let peerId = self.peerId
|
||||
let revoked = self.revoked
|
||||
var lastResult = self.results.last
|
||||
|
||||
if self.forceUpdate {
|
||||
self.forceUpdate = false
|
||||
lastResult = nil
|
||||
}
|
||||
if !self.forceUpdate && self.loadedFromCache {
|
||||
self.populateCache = false
|
||||
self.loadedFromCache = false
|
||||
}
|
||||
let populateCache = self.populateCache
|
||||
|
||||
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<([ExportedInvitation], Int32), NoError> in
|
||||
if let inputPeer = inputPeer {
|
||||
let offsetLink = lastResult?.link
|
||||
let offsetDate = lastResult?.date
|
||||
var flags: Int32 = 0
|
||||
if let _ = offsetLink {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
if revoked {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
let signal = account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetDate: offsetDate, offsetLink: offsetLink, limit: lastResult == nil ? 50 : 100))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([ExportedInvitation], Int32), NoError> in
|
||||
return account.postbox.transaction { transaction -> ([ExportedInvitation], Int32) in
|
||||
guard let result = result else {
|
||||
return ([], 0)
|
||||
}
|
||||
switch result {
|
||||
case let .exportedChatInvites(count, invites, users):
|
||||
var peers: [Peer] = []
|
||||
for apiUser in users {
|
||||
peers.append(TelegramUser(user: apiUser))
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated in
|
||||
return updated
|
||||
})
|
||||
let invitations: [ExportedInvitation] = invites.compactMap { ExportedInvitation(apiExportedInvite: $0) }
|
||||
if populateCache {
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerExportedInvitations, key: CachedPeerExportedInvitations.key(peerId: peerId, revoked: revoked)), entry: CachedPeerExportedInvitations(invitations: invitations, canLoadMore: count >= 50, count: count), collectionSpec: cachedPeerExportedInvitationsCollectionSpec)
|
||||
}
|
||||
return (invitations, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
return signal
|
||||
} else {
|
||||
return .single(([], 0))
|
||||
}
|
||||
}
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] invitations, updatedCount in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.populateCache {
|
||||
strongSelf.populateCache = false
|
||||
strongSelf.results.removeAll()
|
||||
}
|
||||
var existingLinks = Set(strongSelf.results.map { $0.link })
|
||||
for invitation in invitations {
|
||||
if !existingLinks.contains(invitation.link) {
|
||||
strongSelf.results.append(invitation)
|
||||
existingLinks.insert(invitation.link)
|
||||
}
|
||||
}
|
||||
strongSelf.isLoadingMore = false
|
||||
strongSelf.hasLoadedOnce = true
|
||||
strongSelf.canLoadMore = !invitations.isEmpty
|
||||
if strongSelf.canLoadMore {
|
||||
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
|
||||
} else {
|
||||
strongSelf.count = Int32(strongSelf.results.count)
|
||||
}
|
||||
strongSelf.updateState()
|
||||
|
||||
if strongSelf.forceUpdate {
|
||||
strongSelf.loadMore()
|
||||
}
|
||||
}))
|
||||
self.updateState()
|
||||
}
|
||||
|
||||
public func add(_ invite: ExportedInvitation) {
|
||||
var results = self.results
|
||||
results.removeAll(where: { $0.link == invite.link})
|
||||
results.insert(invite, at: 0)
|
||||
self.results = results
|
||||
self.updateState()
|
||||
self.updateCache()
|
||||
}
|
||||
|
||||
public func update(_ invite: ExportedInvitation) {
|
||||
var results = self.results
|
||||
if let index = self.results.firstIndex(where: { $0.link == invite.link }) {
|
||||
results[index] = invite
|
||||
}
|
||||
self.results = results
|
||||
self.updateState()
|
||||
self.updateCache()
|
||||
}
|
||||
|
||||
public func remove(_ invite: ExportedInvitation) {
|
||||
var results = self.results
|
||||
results.removeAll(where: { $0.link == invite.link})
|
||||
self.results = results
|
||||
self.updateState()
|
||||
self.updateCache()
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
self.results = []
|
||||
self.count = 0
|
||||
self.updateState()
|
||||
self.updateCache()
|
||||
}
|
||||
|
||||
private func updateCache() {
|
||||
guard self.hasLoadedOnce && !self.isLoadingMore else {
|
||||
return
|
||||
}
|
||||
|
||||
let peerId = self.peerId
|
||||
let revoked = self.revoked
|
||||
let invitations = Array(self.results.prefix(50))
|
||||
let canLoadMore = self.canLoadMore
|
||||
let count = self.count
|
||||
self.updateDisposable.set(self.account.postbox.transaction({ transaction in
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerExportedInvitations, key: CachedPeerExportedInvitations.key(peerId: peerId, revoked: revoked)), entry: CachedPeerExportedInvitations(invitations: invitations, canLoadMore: canLoadMore, count: count), collectionSpec: cachedPeerExportedInvitationsCollectionSpec)
|
||||
}).start())
|
||||
}
|
||||
|
||||
private func updateState() {
|
||||
self.state.set(.single(PeerExportedInvitationsState(invitations: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerExportedInvitationsContext {
|
||||
private let queue: Queue = Queue()
|
||||
private let impl: QueueLocalObject<PeerExportedInvitationsContextImpl>
|
||||
|
||||
public var state: Signal<PeerExportedInvitationsState, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.state.get().start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public init(account: Account, peerId: PeerId, revoked: Bool, forceUpdate: Bool) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return PeerExportedInvitationsContextImpl(queue: queue, account: account, peerId: peerId, revoked: revoked, forceUpdate: forceUpdate)
|
||||
})
|
||||
}
|
||||
|
||||
public func reload() {
|
||||
self.impl.with { impl in
|
||||
impl.reload()
|
||||
}
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
self.impl.with { impl in
|
||||
impl.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
public func add(_ invite: ExportedInvitation) {
|
||||
self.impl.with { impl in
|
||||
impl.add(invite)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(_ invite: ExportedInvitation) {
|
||||
self.impl.with { impl in
|
||||
impl.update(invite)
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(_ invite: ExportedInvitation) {
|
||||
self.impl.with { impl in
|
||||
impl.remove(invite)
|
||||
}
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
self.impl.with { impl in
|
||||
impl.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private let cachedPeerInvitationImportersCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
|
||||
|
||||
public struct PeerInvitationImportersState: Equatable {
|
||||
@ -262,7 +575,7 @@ public struct PeerInvitationImportersState: Equatable {
|
||||
public var isLoadingMore: Bool
|
||||
public var hasLoadedOnce: Bool
|
||||
public var canLoadMore: Bool
|
||||
public var count: Int
|
||||
public var count: Int32
|
||||
}
|
||||
|
||||
final class CachedPeerInvitationImporters: PostboxCoding {
|
||||
@ -330,8 +643,9 @@ private final class PeerInvitationImportersContextImpl {
|
||||
private var isLoadingMore: Bool = false
|
||||
private var hasLoadedOnce: Bool = false
|
||||
private var canLoadMore: Bool = true
|
||||
private var loadedFromCache = false
|
||||
private var results: [PeerInvitationImportersState.Importer] = []
|
||||
private var count: Int
|
||||
private var count: Int32
|
||||
private var populateCache: Bool = true
|
||||
|
||||
let state = Promise<PeerInvitationImportersState>()
|
||||
@ -342,7 +656,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
self.peerId = peerId
|
||||
self.link = invite.link
|
||||
|
||||
let count = invite.count.flatMap { Int($0) } ?? 0
|
||||
let count = invite.count ?? 0
|
||||
self.count = count
|
||||
|
||||
self.isLoadingMore = true
|
||||
@ -371,6 +685,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
strongSelf.results = cachedPeers
|
||||
strongSelf.hasLoadedOnce = true
|
||||
strongSelf.canLoadMore = canLoadMore
|
||||
strongSelf.loadedFromCache = true
|
||||
}
|
||||
strongSelf.loadMore()
|
||||
}))
|
||||
@ -390,12 +705,18 @@ private final class PeerInvitationImportersContextImpl {
|
||||
let account = self.account
|
||||
let peerId = self.peerId
|
||||
let link = self.link
|
||||
let lastResult = self.results.last
|
||||
let populateCache = self.populateCache
|
||||
|
||||
var lastResult = self.results.last
|
||||
if self.loadedFromCache {
|
||||
self.loadedFromCache = false
|
||||
lastResult = nil
|
||||
}
|
||||
|
||||
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in
|
||||
|> mapToSignal { inputPeer -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
|
||||
if let inputPeer = inputPeer {
|
||||
let offsetUser = lastResult?.peer.peer.flatMap { apiInputUser($0) } ?? .inputUserEmpty
|
||||
let offsetDate = lastResult?.date ?? 0
|
||||
@ -404,8 +725,8 @@ private final class PeerInvitationImportersContextImpl {
|
||||
|> `catch` { _ -> Signal<Api.messages.ChatInviteImporters?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int), NoError> in
|
||||
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int) in
|
||||
|> mapToSignal { result -> Signal<([PeerInvitationImportersState.Importer], Int32), NoError> in
|
||||
return account.postbox.transaction { transaction -> ([PeerInvitationImportersState.Importer], Int32) in
|
||||
guard let result = result else {
|
||||
return ([], 0)
|
||||
}
|
||||
@ -434,7 +755,7 @@ private final class PeerInvitationImportersContextImpl {
|
||||
if populateCache {
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerInvitationImporters, key: CachedPeerInvitationImporters.key(peerId: peerId, link: link)), entry: CachedPeerInvitationImporters(importers: resultImporters, count: count), collectionSpec: cachedPeerInvitationImportersCollectionSpec)
|
||||
}
|
||||
return (resultImporters, Int(count))
|
||||
return (resultImporters, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -462,9 +783,9 @@ private final class PeerInvitationImportersContextImpl {
|
||||
strongSelf.hasLoadedOnce = true
|
||||
strongSelf.canLoadMore = !importers.isEmpty
|
||||
if strongSelf.canLoadMore {
|
||||
strongSelf.count = max(updatedCount, strongSelf.results.count)
|
||||
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count))
|
||||
} else {
|
||||
strongSelf.count = strongSelf.results.count
|
||||
strongSelf.count = Int32(strongSelf.results.count)
|
||||
}
|
||||
strongSelf.updateState()
|
||||
}))
|
||||
|
@ -14,6 +14,14 @@ public struct TelegramPeerPhoto {
|
||||
public let index: Int
|
||||
public let totalCount: Int
|
||||
public let messageId: MessageId?
|
||||
public init(image: TelegramMediaImage, reference: TelegramMediaImageReference?, date: Int32, index: Int, totalCount: Int, messageId: MessageId?) {
|
||||
self.image = image
|
||||
self.reference = reference
|
||||
self.date = date
|
||||
self.index = index
|
||||
self.totalCount = totalCount
|
||||
self.messageId = messageId
|
||||
}
|
||||
}
|
||||
|
||||
public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> {
|
||||
|
@ -5,6 +5,26 @@ import SwiftSignalKit
|
||||
|
||||
import SyncCore
|
||||
|
||||
private struct SearchStickersConfiguration {
|
||||
static var defaultValue: SearchStickersConfiguration {
|
||||
return SearchStickersConfiguration(cacheTimeout: 86400)
|
||||
}
|
||||
|
||||
public let cacheTimeout: Int32
|
||||
|
||||
fileprivate init(cacheTimeout: Int32) {
|
||||
self.cacheTimeout = cacheTimeout
|
||||
}
|
||||
|
||||
static func with(appConfiguration: AppConfiguration) -> SearchStickersConfiguration {
|
||||
if let data = appConfiguration.data, let value = data["stickers_emoji_cache_time"] as? Int32 {
|
||||
return SearchStickersConfiguration(cacheTimeout: value)
|
||||
} else {
|
||||
return .defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class FoundStickerItem: Equatable {
|
||||
public let file: TelegramMediaFile
|
||||
public let stringRepresentations: [String]
|
||||
@ -140,7 +160,15 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
|
||||
result.append(contentsOf: installedItems)
|
||||
}
|
||||
|
||||
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query))) as? CachedStickerQueryResult
|
||||
var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query))) as? CachedStickerQueryResult
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue
|
||||
let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration)
|
||||
|
||||
if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout {
|
||||
cached = nil
|
||||
}
|
||||
|
||||
return (result, cached)
|
||||
} |> mapToSignal { localItems, cached -> Signal<[FoundStickerItem], NoError> in
|
||||
@ -199,7 +227,8 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker
|
||||
result.append(contentsOf: animatedItems)
|
||||
result.append(contentsOf: items)
|
||||
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)), entry: CachedStickerQueryResult(items: files, hash: hash), collectionSpec: collectionSpec)
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query)), entry: CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime), collectionSpec: collectionSpec)
|
||||
|
||||
return result
|
||||
case .stickersNotModified:
|
||||
|
@ -6,6 +6,7 @@ import SyncCore
|
||||
|
||||
public enum ServerProvidedSuggestion: String {
|
||||
case autoarchivePopular = "AUTOARCHIVE_POPULAR"
|
||||
case newcomerTicks = "NEWCOMER_TICKS"
|
||||
}
|
||||
|
||||
public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProvidedSuggestion], NoError> {
|
||||
@ -22,12 +23,7 @@ public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProv
|
||||
return []
|
||||
}
|
||||
return list.compactMap { item -> ServerProvidedSuggestion? in
|
||||
switch item {
|
||||
case "AUTOARCHIVE_POPULAR":
|
||||
return .autoarchivePopular
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return ServerProvidedSuggestion(rawValue: item)
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
@ -205,10 +205,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
|
||||
|
||||
let peerStatusSettings = PeerStatusSettings(apiSettings: userFull.settings)
|
||||
|
||||
var hasScheduledMessages = false
|
||||
if (userFull.flags & 1 << 12) != 0 {
|
||||
hasScheduledMessages = true
|
||||
}
|
||||
let hasScheduledMessages = (userFull.flags & 1 << 12) != 0
|
||||
|
||||
return previous.withUpdatedAbout(userFull.about).withUpdatedBotInfo(botInfo).withUpdatedCommonGroupCount(userFull.commonChatsCount).withUpdatedIsBlocked(isBlocked).withUpdatedVoiceCallsAvailable(voiceCallsAvailable).withUpdatedVideoCallsAvailable(videoCallsAvailable).withUpdatedCallsPrivate(callsPrivate).withUpdatedCanPinMessages(canPinMessages).withUpdatedPeerStatusSettings(peerStatusSettings).withUpdatedPinnedMessageId(pinnedMessageId).withUpdatedHasScheduledMessages(hasScheduledMessages)
|
||||
}
|
||||
@ -257,7 +254,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI
|
||||
|
||||
let photo: TelegramMediaImage? = chatFull.chatPhoto.flatMap(telegramMediaImageFromApiPhoto)
|
||||
|
||||
let exportedInvitation = chatFull.exportedInvite.flatMap(ExportedInvitation.init(apiExportedInvite:))
|
||||
let exportedInvitation = chatFull.exportedInvite.flatMap { ExportedInvitation(apiExportedInvite: $0) }
|
||||
let pinnedMessageId = chatFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) })
|
||||
|
||||
var peers: [Peer] = []
|
||||
|
File diff suppressed because it is too large
Load Diff
12
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_biglink.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/ic_biglink.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Links/LargeLink.imageset/ic_biglink.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Links/Share.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Links/Share.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_sharelink.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Links/Share.imageset/ic_sharelink.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Links/Share.imageset/ic_sharelink.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -305,6 +305,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
private var applicationInForegroundDisposable: Disposable?
|
||||
private var applicationInFocusDisposable: Disposable?
|
||||
|
||||
private let checksTooltipDisposable = MetaDisposable()
|
||||
private var shouldDisplayChecksTooltip = false
|
||||
|
||||
private var checkedPeerChatServiceActions = false
|
||||
|
||||
private var willAppear = false
|
||||
@ -3424,6 +3427,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.peekTimerDisposable.dispose()
|
||||
self.hasActiveGroupCallDisposable?.dispose()
|
||||
self.createVoiceChatDisposable.dispose()
|
||||
self.checksTooltipDisposable.dispose()
|
||||
}
|
||||
|
||||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||||
@ -4761,7 +4765,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
|
||||
let editingMessage = strongSelf.editingMessage
|
||||
let text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText))
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
var entitiesAttribute: TextEntitiesMessageAttribute?
|
||||
@ -6236,7 +6239,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.openScheduledMessages()
|
||||
}
|
||||
|
||||
// strongSelf.displayChecksTooltip()
|
||||
if strongSelf.shouldDisplayChecksTooltip {
|
||||
strongSelf.displayChecksTooltip()
|
||||
strongSelf.shouldDisplayChecksTooltip = false
|
||||
strongSelf.checksTooltipDisposable.set(dismissServerProvidedSuggestion(account: strongSelf.context.account, suggestion: .newcomerTicks).start())
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@ -6482,7 +6489,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return false
|
||||
}
|
||||
|
||||
if !strongSelf.context.sharedContext.currentMediaInputSettings.with { $0.enableRaiseToSpeak } {
|
||||
if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -6707,6 +6714,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}))
|
||||
}
|
||||
|
||||
self.checksTooltipDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] values in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !values.contains(.newcomerTicks) {
|
||||
return
|
||||
}
|
||||
strongSelf.shouldDisplayChecksTooltip = true
|
||||
}))
|
||||
|
||||
if self.scheduledActivateInput {
|
||||
self.scheduledActivateInput = false
|
||||
|
||||
@ -7481,7 +7499,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] result in
|
||||
controller?.dismiss()
|
||||
|
||||
guard let strongSelf = self, case let .result(stats) = result, var categories = stats.media[peer.id] else {
|
||||
guard let strongSelf = self, case let .result(stats) = result, let categories = stats.media[peer.id] else {
|
||||
return
|
||||
}
|
||||
let presentationData = strongSelf.presentationData
|
||||
@ -7682,16 +7700,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) {
|
||||
if let message = messages.first, case let .message(desc) = message, let mediaReference = desc.mediaReference {
|
||||
if let message = messages.first, case let .message(text, _, maybeMediaReference, _, _) = message, let mediaReference = maybeMediaReference {
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||||
var state = state
|
||||
if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty {
|
||||
state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
|
||||
}
|
||||
if !desc.text.isEmpty {
|
||||
if !text.isEmpty {
|
||||
state = state.updatedInterfaceState { state in
|
||||
if let editMessage = state.editMessage {
|
||||
return state.withUpdatedEditMessage(editMessage.withUpdatedInputState(ChatTextInputState(inputText: NSAttributedString(string: desc.text))))
|
||||
return state.withUpdatedEditMessage(editMessage.withUpdatedInputState(ChatTextInputState(inputText: NSAttributedString(string: text))))
|
||||
}
|
||||
return state
|
||||
}
|
||||
@ -9338,7 +9356,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let complete = results.completed
|
||||
var navigateIndex: MessageIndex?
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||||
if let data = current.search {
|
||||
@ -10113,8 +10130,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
case let .withBotStartPayload(botStart):
|
||||
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(peerId), botStart: botStart))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -10536,7 +10551,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) {
|
||||
self.commitPurposefulAction()
|
||||
|
||||
self.presentVoiceMessageDiscardAlert(action: {
|
||||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||||
if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback {
|
||||
if url.hasSuffix(".m3u8") {
|
||||
let navigationController = self.navigationController as? NavigationController
|
||||
|
@ -159,6 +159,7 @@ final class PeerInfoScreenData {
|
||||
let members: PeerInfoMembersData?
|
||||
let encryptionKeyFingerprint: SecretChatKeyFingerprint?
|
||||
let globalSettings: TelegramGlobalSettings?
|
||||
let invitations: PeerExportedInvitationsState?
|
||||
|
||||
init(
|
||||
peer: Peer?,
|
||||
@ -172,7 +173,8 @@ final class PeerInfoScreenData {
|
||||
linkedDiscussionPeer: Peer?,
|
||||
members: PeerInfoMembersData?,
|
||||
encryptionKeyFingerprint: SecretChatKeyFingerprint?,
|
||||
globalSettings: TelegramGlobalSettings?
|
||||
globalSettings: TelegramGlobalSettings?,
|
||||
invitations: PeerExportedInvitationsState?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.cachedData = cachedData
|
||||
@ -186,6 +188,7 @@ final class PeerInfoScreenData {
|
||||
self.members = members
|
||||
self.encryptionKeyFingerprint = encryptionKeyFingerprint
|
||||
self.globalSettings = globalSettings
|
||||
self.invitations = invitations
|
||||
}
|
||||
}
|
||||
|
||||
@ -442,7 +445,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account
|
||||
linkedDiscussionPeer: nil,
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: globalSettings
|
||||
globalSettings: globalSettings,
|
||||
invitations: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -464,7 +468,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
linkedDiscussionPeer: nil,
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: nil
|
||||
globalSettings: nil,
|
||||
invitations: nil
|
||||
))
|
||||
case let .user(userPeerId, secretChatId, kind):
|
||||
let groupsInCommon: GroupsInCommonContext?
|
||||
@ -603,13 +608,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
linkedDiscussionPeer: nil,
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: encryptionKeyFingerprint,
|
||||
globalSettings: nil
|
||||
globalSettings: nil,
|
||||
invitations: nil
|
||||
)
|
||||
}
|
||||
case .channel:
|
||||
let status = context.account.viewTracker.peerView(peerId, updateData: false)
|
||||
|> map { peerView -> PeerInfoStatusData? in
|
||||
guard let channel = peerView.peers[peerId] as? TelegramChannel else {
|
||||
guard let _ = peerView.peers[peerId] as? TelegramChannel else {
|
||||
return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false)
|
||||
}
|
||||
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 {
|
||||
@ -623,13 +629,19 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
|
||||
var combinedKeys: [PostboxViewKey] = []
|
||||
combinedKeys.append(globalNotificationsKey)
|
||||
|
||||
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
|
||||
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
|
||||
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId),
|
||||
context.account.postbox.combinedView(keys: combinedKeys),
|
||||
status
|
||||
status,
|
||||
invitationsContextPromise.get(),
|
||||
invitationsStatePromise.get()
|
||||
)
|
||||
|> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in
|
||||
|> map { peerView, availablePanes, combinedView, status, currentInvitationsContext, invitations -> PeerInfoScreenData in
|
||||
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
|
||||
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
|
||||
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
|
||||
@ -642,6 +654,18 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
discussionPeer = peer
|
||||
}
|
||||
|
||||
if currentInvitationsContext == nil {
|
||||
var canManageInvitations = false
|
||||
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || ((channel.adminRights != nil && channel.hasPermission(.pinMessages)) && cachedData.flags.contains(.canChangeUsername)) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
if canManageInvitations {
|
||||
let invitationsContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: true)
|
||||
invitationsContextPromise.set(.single(invitationsContext))
|
||||
invitationsStatePromise.set(invitationsContext.state |> map(Optional.init))
|
||||
}
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
peer: peerView.peers[peerId],
|
||||
cachedData: peerView.cachedData,
|
||||
@ -654,7 +678,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
linkedDiscussionPeer: discussionPeer,
|
||||
members: nil,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: nil
|
||||
globalSettings: nil,
|
||||
invitations: invitations
|
||||
)
|
||||
}
|
||||
case let .group(groupId):
|
||||
@ -751,14 +776,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
|
||||
var combinedKeys: [PostboxViewKey] = []
|
||||
combinedKeys.append(globalNotificationsKey)
|
||||
|
||||
let invitationsContextPromise = Promise<PeerExportedInvitationsContext?>(nil)
|
||||
let invitationsStatePromise = Promise<PeerExportedInvitationsState?>(nil)
|
||||
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
context.account.viewTracker.peerView(groupId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: groupId),
|
||||
context.account.postbox.combinedView(keys: combinedKeys),
|
||||
status,
|
||||
membersData
|
||||
membersData,
|
||||
invitationsContextPromise.get(),
|
||||
invitationsStatePromise.get()
|
||||
)
|
||||
|> map { peerView, availablePanes, combinedView, status, membersData -> PeerInfoScreenData in
|
||||
|> map { peerView, availablePanes, combinedView, status, membersData, currentInvitationsContext, invitations -> PeerInfoScreenData in
|
||||
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
|
||||
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
|
||||
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
|
||||
@ -780,6 +811,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
}
|
||||
}
|
||||
|
||||
if currentInvitationsContext == nil {
|
||||
var canManageInvitations = false
|
||||
if let group = peerViewMainPeer(peerView) as? TelegramGroup, case .creator = group.role {
|
||||
canManageInvitations = true
|
||||
} else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData, channel.flags.contains(.isCreator) || ((channel.adminRights != nil && channel.hasPermission(.pinMessages)) && cachedData.flags.contains(.canChangeUsername)) {
|
||||
canManageInvitations = true
|
||||
}
|
||||
if canManageInvitations {
|
||||
let invitationsContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: true)
|
||||
invitationsContextPromise.set(.single(invitationsContext))
|
||||
invitationsStatePromise.set(invitationsContext.state |> map(Optional.init))
|
||||
}
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
peer: peerView.peers[groupId],
|
||||
cachedData: peerView.cachedData,
|
||||
@ -792,7 +837,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
linkedDiscussionPeer: discussionPeer,
|
||||
members: membersData,
|
||||
encryptionKeyFingerprint: nil,
|
||||
globalSettings: nil
|
||||
globalSettings: nil,
|
||||
invitations: invitations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1185,6 +1185,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
discussionGroupTitle = "..."
|
||||
}
|
||||
|
||||
let invitesText: String
|
||||
if let count = data.invitations?.count, count > 0 {
|
||||
invitesText = "\(count)"
|
||||
} else {
|
||||
invitesText = ""
|
||||
}
|
||||
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
|
||||
interaction.editingOpenInviteLinksSetup()
|
||||
}))
|
||||
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemDiscussionGroup, label: .text(discussionGroupTitle), text: presentationData.strings.Channel_DiscussionGroup, action: {
|
||||
interaction.editingOpenDiscussionGroupSetup()
|
||||
}))
|
||||
@ -1255,12 +1266,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
interaction.editingOpenPublicLinkSetup()
|
||||
}))
|
||||
|
||||
if !isPublic {
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(""), text: presentationData.strings.GroupInfo_InviteLinks, action: {
|
||||
interaction.editingOpenInviteLinksSetup()
|
||||
}))
|
||||
let invitesText: String
|
||||
if let count = data.invitations?.count, count > 0 {
|
||||
invitesText = "\(count)"
|
||||
} else {
|
||||
invitesText = ""
|
||||
}
|
||||
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
|
||||
interaction.editingOpenInviteLinksSetup()
|
||||
}))
|
||||
|
||||
if let linkedDiscussionPeer = data.linkedDiscussionPeer {
|
||||
let peerTitle: String
|
||||
if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty {
|
||||
@ -1318,9 +1334,10 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
}
|
||||
} else if let group = data.peer as? TelegramGroup {
|
||||
let ItemUsername = 1
|
||||
let ItemPreHistory = 2
|
||||
let ItemPermissions = 3
|
||||
let ItemAdmins = 4
|
||||
let ItemInviteLinks = 2
|
||||
let ItemPreHistory = 3
|
||||
let ItemPermissions = 4
|
||||
let ItemAdmins = 5
|
||||
|
||||
if case .creator = group.role {
|
||||
if let cachedData = data.cachedData as? CachedGroupData {
|
||||
@ -1330,6 +1347,18 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
let invitesText: String
|
||||
if let count = data.invitations?.count, count > 0 {
|
||||
invitesText = "\(count)"
|
||||
} else {
|
||||
invitesText = ""
|
||||
}
|
||||
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
|
||||
interaction.editingOpenInviteLinksSetup()
|
||||
}))
|
||||
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistory, action: {
|
||||
interaction.editingOpenPreHistorySetup()
|
||||
}))
|
||||
@ -4540,15 +4569,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let mode: ChannelVisibilityControllerMode
|
||||
if groupPeer.addressName != nil {
|
||||
let visibilityController = channelVisibilityController(context: strongSelf.context, peerId: groupPeer.id, mode: .generic, upgradedToSupergroup: { _, f in f() }, onDismissRemoveController: contactsController)
|
||||
//visibilityController.navigationPresentation = .modal
|
||||
|
||||
contactsController?.push(visibilityController)
|
||||
} else {
|
||||
contactsController?.push(InviteLinkInviteController(context: context, peerId: groupPeer.id))
|
||||
}
|
||||
strongSelf.view.endEditing(true)
|
||||
contactsController?.present(InviteLinkInviteController(context: context, peerId: groupPeer.id, parentNavigationController: contactsController?.navigationController as? NavigationController), in: .window(.root))
|
||||
}
|
||||
|
||||
strongSelf.controller?.push(contactsController)
|
||||
|
@ -70,9 +70,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.actionArea)
|
||||
|
||||
self.messageDisposable.set((context.account.postbox.messageAtId(messageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] message in
|
||||
self.messageDisposable.set((context.account.postbox.messageView(messageId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] messageView in
|
||||
if let strongSelf = self {
|
||||
let message = messageView.message
|
||||
var authorName = ""
|
||||
var text = ""
|
||||
if let author = message?.effectiveAuthor {
|
||||
|
@ -91,8 +91,8 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
|
||||
}
|
||||
|
||||
let resolvedWallpaper: Signal<TelegramWallpaper?, NoError>
|
||||
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
|
||||
resolvedWallpaper = cachedWallpaper(account: account, slug: file.slug, settings: file.settings)
|
||||
if case let .file(id, _, _, _, _, _, slug, _, settings) = presentationTheme.chat.defaultWallpaper, id == 0 {
|
||||
resolvedWallpaper = cachedWallpaper(account: account, slug: slug, settings: settings)
|
||||
|> map { wallpaper in
|
||||
return wallpaper?.wallpaper
|
||||
}
|
||||
@ -102,15 +102,15 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager {
|
||||
|
||||
return resolvedWallpaper
|
||||
|> mapToSignal { wallpaper -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in
|
||||
if let wallpaper = wallpaper, case let .file(file) = wallpaper {
|
||||
if let wallpaper = wallpaper, case let .file(_, _, _, _, _, _, slug, file, _) = wallpaper {
|
||||
var convertedRepresentations: [ImageRepresentationWithReference] = []
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
|
||||
return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
|
||||
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(slug), resource: file.resource)))
|
||||
return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
|
||||
|> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in
|
||||
guard complete, let fullSizeData = fullSizeData else {
|
||||
return .complete()
|
||||
}
|
||||
accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true)
|
||||
accountManager.mediaBox.storeResourceData(file.resource.id, data: fullSizeData, synchronous: true)
|
||||
return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme))
|
||||
}
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user