mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
598 lines
25 KiB
Swift
598 lines
25 KiB
Swift
import Foundation
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
private final class UserInfoControllerArguments {
|
|
let account: Account
|
|
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
|
|
let openChat: () -> Void
|
|
let changeNotificationMuteSettings: () -> Void
|
|
let openSharedMedia: () -> Void
|
|
let openGroupsInCommon: () -> Void
|
|
let updatePeerBlocked: (Bool) -> Void
|
|
let deleteContact: () -> Void
|
|
|
|
init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void) {
|
|
self.account = account
|
|
self.updateEditingName = updateEditingName
|
|
self.openChat = openChat
|
|
self.changeNotificationMuteSettings = changeNotificationMuteSettings
|
|
self.openSharedMedia = openSharedMedia
|
|
self.openGroupsInCommon = openGroupsInCommon
|
|
self.updatePeerBlocked = updatePeerBlocked
|
|
self.deleteContact = deleteContact
|
|
}
|
|
}
|
|
|
|
private enum UserInfoSection: ItemListSectionId {
|
|
case info
|
|
case actions
|
|
case sharedMediaAndNotifications
|
|
case block
|
|
}
|
|
|
|
private enum UserInfoEntry: ItemListNodeEntry {
|
|
case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState)
|
|
case about(text: String)
|
|
case phoneNumber(index: Int, value: PhoneNumberWithLabel)
|
|
case userName(value: String)
|
|
case sendMessage
|
|
case shareContact
|
|
case startSecretChat
|
|
case sharedMedia
|
|
case notifications(settings: PeerNotificationSettings?)
|
|
case notificationSound(settings: PeerNotificationSettings?)
|
|
case groupsInCommon(Int32)
|
|
case secretEncryptionKey(SecretChatKeyFingerprint)
|
|
case block(action: DestructiveUserInfoAction)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .info, .about, .phoneNumber, .userName:
|
|
return UserInfoSection.info.rawValue
|
|
case .sendMessage, .shareContact, .startSecretChat:
|
|
return UserInfoSection.actions.rawValue
|
|
case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey, .groupsInCommon:
|
|
return UserInfoSection.sharedMediaAndNotifications.rawValue
|
|
case .block:
|
|
return UserInfoSection.block.rawValue
|
|
}
|
|
}
|
|
|
|
var stableId: Int {
|
|
return self.sortIndex
|
|
}
|
|
|
|
static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool {
|
|
switch lhs {
|
|
case let .info(lhsPeer, lhsPresence, lhsCachedData, lhsState):
|
|
switch rhs {
|
|
case let .info(rhsPeer, rhsPresence, rhsCachedData, rhsState):
|
|
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
|
if !lhsPeer.isEqual(rhsPeer) {
|
|
return false
|
|
}
|
|
} else if (lhsPeer != nil) != (rhsPeer != nil) {
|
|
return false
|
|
}
|
|
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
|
if !lhsPresence.isEqual(to: rhsPresence) {
|
|
return false
|
|
}
|
|
} else if (lhsPresence != nil) != (rhsPresence != nil) {
|
|
return false
|
|
}
|
|
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
|
|
if !lhsCachedData.isEqual(to: rhsCachedData) {
|
|
return false
|
|
}
|
|
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
|
|
return false
|
|
}
|
|
if lhsState != rhsState {
|
|
return false
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .about(lhsText):
|
|
switch rhs {
|
|
case .about(lhsText):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .phoneNumber(lhsIndex, lhsValue):
|
|
switch rhs {
|
|
case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .userName(value):
|
|
switch rhs {
|
|
case .userName(value):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case .sendMessage:
|
|
switch rhs {
|
|
case .sendMessage:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case .shareContact:
|
|
switch rhs {
|
|
case .shareContact:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case .startSecretChat:
|
|
switch rhs {
|
|
case .startSecretChat:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case .sharedMedia:
|
|
switch rhs {
|
|
case .sharedMedia:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .notifications(lhsSettings):
|
|
switch rhs {
|
|
case let .notifications(rhsSettings):
|
|
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
|
|
return lhsSettings.isEqual(to: rhsSettings)
|
|
} else if (lhsSettings != nil) != (rhsSettings != nil) {
|
|
return false
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .notificationSound(lhsSettings):
|
|
switch rhs {
|
|
case let .notificationSound(rhsSettings):
|
|
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
|
|
return lhsSettings.isEqual(to: rhsSettings)
|
|
} else if (lhsSettings != nil) != (rhsSettings != nil) {
|
|
return false
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .groupsInCommon(count):
|
|
if case .groupsInCommon(count) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .secretEncryptionKey(fingerprint):
|
|
if case .secretEncryptionKey(fingerprint) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .block(action):
|
|
switch rhs {
|
|
case .block(action):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private var sortIndex: Int {
|
|
switch self {
|
|
case .info:
|
|
return 0
|
|
case .about:
|
|
return 1
|
|
case let .phoneNumber(index, _):
|
|
return 2 + index
|
|
case .userName:
|
|
return 1000
|
|
case .sendMessage:
|
|
return 1001
|
|
case .shareContact:
|
|
return 1002
|
|
case .startSecretChat:
|
|
return 1003
|
|
case .sharedMedia:
|
|
return 1004
|
|
case .notifications:
|
|
return 1005
|
|
case .notificationSound:
|
|
return 1006
|
|
case .groupsInCommon:
|
|
return 1007
|
|
case .secretEncryptionKey:
|
|
return 1008
|
|
case .block:
|
|
return 1009
|
|
}
|
|
}
|
|
|
|
static func <(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool {
|
|
return lhs.sortIndex < rhs.sortIndex
|
|
}
|
|
|
|
func item(_ arguments: UserInfoControllerArguments) -> ListViewItem {
|
|
switch self {
|
|
case let .info(peer, presence, cachedData, state):
|
|
return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
|
|
arguments.updateEditingName(editingName)
|
|
})
|
|
case let .about(text):
|
|
return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section)
|
|
case let .phoneNumber(_, value):
|
|
return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section)
|
|
case let .userName(value):
|
|
return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section)
|
|
case .sendMessage:
|
|
return ItemListActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
|
|
arguments.openChat()
|
|
})
|
|
case .shareContact:
|
|
return ItemListActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
|
|
|
|
})
|
|
case .startSecretChat:
|
|
return ItemListActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
|
|
|
|
})
|
|
case .sharedMedia:
|
|
return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: {
|
|
arguments.openSharedMedia()
|
|
})
|
|
case let .notifications(settings):
|
|
let label: String
|
|
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
|
|
label = "Disabled"
|
|
} else {
|
|
label = "Enabled"
|
|
}
|
|
return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .plain, action: {
|
|
arguments.changeNotificationMuteSettings()
|
|
})
|
|
case let .notificationSound(settings):
|
|
let label: String
|
|
label = "Default"
|
|
return ItemListDisclosureItem(title: "Sound", label: label, sectionId: self.section, style: .plain, action: {
|
|
})
|
|
case let .groupsInCommon(count):
|
|
return ItemListDisclosureItem(title: "Groups in Common", label: "\(count)", sectionId: self.section, style: .plain, action: {
|
|
arguments.openGroupsInCommon()
|
|
})
|
|
case let .secretEncryptionKey(fingerprint):
|
|
return ItemListDisclosureItem(title: "Encryption Key", label: "", sectionId: self.section, style: .plain, action: {
|
|
})
|
|
case let .block(action):
|
|
let title: String
|
|
switch action {
|
|
case .block:
|
|
title = "Block User"
|
|
case .unblock:
|
|
title = "Unblock User"
|
|
case .removeContact:
|
|
title = "Remove Contact"
|
|
}
|
|
return ItemListActionItem(title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: {
|
|
switch action {
|
|
case .block:
|
|
arguments.updatePeerBlocked(true)
|
|
case .unblock:
|
|
arguments.updatePeerBlocked(false)
|
|
case .removeContact:
|
|
arguments.deleteContact()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum DestructiveUserInfoAction {
|
|
case block
|
|
case removeContact
|
|
case unblock
|
|
}
|
|
|
|
private struct UserInfoEditingState: Equatable {
|
|
let editingName: ItemListAvatarAndNameInfoItemName?
|
|
|
|
static func ==(lhs: UserInfoEditingState, rhs: UserInfoEditingState) -> Bool {
|
|
if lhs.editingName != rhs.editingName {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private struct UserInfoState: Equatable {
|
|
let savingData: Bool
|
|
let editingState: UserInfoEditingState?
|
|
|
|
init() {
|
|
self.savingData = false
|
|
self.editingState = nil
|
|
}
|
|
|
|
init(savingData: Bool, editingState: UserInfoEditingState?) {
|
|
self.savingData = savingData
|
|
self.editingState = editingState
|
|
}
|
|
|
|
static func ==(lhs: UserInfoState, rhs: UserInfoState) -> Bool {
|
|
if lhs.savingData != rhs.savingData {
|
|
return false
|
|
}
|
|
if lhs.editingState != rhs.editingState {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func withUpdatedSavingData(_ savingData: Bool) -> UserInfoState {
|
|
return UserInfoState(savingData: savingData, editingState: self.editingState)
|
|
}
|
|
|
|
func withUpdatedEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState {
|
|
return UserInfoState(savingData: self.savingData, editingState: editingState)
|
|
}
|
|
}
|
|
|
|
private func userInfoEntries(account: Account, view: PeerView, state: UserInfoState, peerChatState: Coding?) -> [UserInfoEntry] {
|
|
var entries: [UserInfoEntry] = []
|
|
|
|
guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else {
|
|
return []
|
|
}
|
|
|
|
var editingName: ItemListAvatarAndNameInfoItemName?
|
|
|
|
var isEditing = false
|
|
if let editingState = state.editingState {
|
|
isEditing = true
|
|
|
|
if view.peerIsContact {
|
|
editingName = editingState.editingName
|
|
}
|
|
}
|
|
|
|
entries.append(UserInfoEntry.info(peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil)))
|
|
if let cachedUserData = view.cachedData as? CachedUserData {
|
|
if let about = cachedUserData.about, !about.isEmpty {
|
|
entries.append(UserInfoEntry.about(text: about))
|
|
}
|
|
}
|
|
|
|
if let phoneNumber = user.phone, !phoneNumber.isEmpty {
|
|
entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber)))
|
|
}
|
|
|
|
if !isEditing {
|
|
if let username = user.username, !username.isEmpty {
|
|
entries.append(UserInfoEntry.userName(value: username))
|
|
}
|
|
|
|
if !(peer is TelegramSecretChat) {
|
|
entries.append(UserInfoEntry.sendMessage)
|
|
if view.peerIsContact {
|
|
entries.append(UserInfoEntry.shareContact)
|
|
}
|
|
entries.append(UserInfoEntry.startSecretChat)
|
|
}
|
|
entries.append(UserInfoEntry.sharedMedia)
|
|
}
|
|
entries.append(UserInfoEntry.notifications(settings: view.notificationSettings))
|
|
if let groupsInCommon = (view.cachedData as? CachedUserData)?.commonGroupCount, groupsInCommon != 0 && !isEditing {
|
|
entries.append(UserInfoEntry.groupsInCommon(groupsInCommon))
|
|
}
|
|
|
|
if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint {
|
|
entries.append(UserInfoEntry.secretEncryptionKey(keyFingerprint))
|
|
}
|
|
|
|
if isEditing {
|
|
entries.append(UserInfoEntry.notificationSound(settings: view.notificationSettings))
|
|
if view.peerIsContact {
|
|
entries.append(UserInfoEntry.block(action: .removeContact))
|
|
}
|
|
} else {
|
|
if let cachedData = view.cachedData as? CachedUserData {
|
|
if cachedData.isBlocked {
|
|
entries.append(UserInfoEntry.block(action: .unblock))
|
|
} else {
|
|
entries.append(UserInfoEntry.block(action: .block))
|
|
}
|
|
}
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
public func userInfoController(account: Account, peerId: PeerId) -> ViewController {
|
|
let statePromise = ValuePromise(UserInfoState(), ignoreRepeated: true)
|
|
let stateValue = Atomic(value: UserInfoState())
|
|
let updateState: ((UserInfoState) -> UserInfoState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
var pushControllerImpl: ((ViewController) -> Void)?
|
|
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)?
|
|
var openChatImpl: (() -> Void)?
|
|
|
|
let actionsDisposable = DisposableSet()
|
|
|
|
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
actionsDisposable.add(account.viewTracker.updatedCachedChannelParticipants(peerId, forceImmediateUpdate: true).start())
|
|
}
|
|
|
|
let updatePeerNameDisposable = MetaDisposable()
|
|
actionsDisposable.add(updatePeerNameDisposable)
|
|
|
|
let updatePeerBlockedDisposable = MetaDisposable()
|
|
actionsDisposable.add(updatePeerBlockedDisposable)
|
|
|
|
let changeMuteSettingsDisposable = MetaDisposable()
|
|
actionsDisposable.add(changeMuteSettingsDisposable)
|
|
|
|
let arguments = UserInfoControllerArguments(account: account, updateEditingName: { editingName in
|
|
updateState { state in
|
|
if let _ = state.editingState {
|
|
return state.withUpdatedEditingState(UserInfoEditingState(editingName: editingName))
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
}, openChat: {
|
|
openChatImpl?()
|
|
}, changeNotificationMuteSettings: {
|
|
let controller = ActionSheetController()
|
|
let dismissAction: () -> Void = { [weak controller] in
|
|
controller?.dismissAnimated()
|
|
}
|
|
let notificationAction: (Int32) -> Void = { muteUntil in
|
|
let muteState: PeerMuteState
|
|
if muteUntil <= 0 {
|
|
muteState = .unmuted
|
|
} else if muteUntil == Int32.max {
|
|
muteState = .muted(until: Int32.max)
|
|
} else {
|
|
muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil)
|
|
}
|
|
changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start())
|
|
}
|
|
controller.setItemGroups([
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: "Enable", action: {
|
|
dismissAction()
|
|
notificationAction(0)
|
|
}),
|
|
ActionSheetButtonItem(title: "Mute for 1 hour", action: {
|
|
dismissAction()
|
|
notificationAction(1 * 60 * 60)
|
|
}),
|
|
ActionSheetButtonItem(title: "Mute for 8 hours", action: {
|
|
dismissAction()
|
|
notificationAction(8 * 60 * 60)
|
|
}),
|
|
ActionSheetButtonItem(title: "Mute for 2 days", action: {
|
|
dismissAction()
|
|
notificationAction(2 * 24 * 60 * 60)
|
|
}),
|
|
ActionSheetButtonItem(title: "Disable", action: {
|
|
dismissAction()
|
|
notificationAction(Int32.max)
|
|
})
|
|
]),
|
|
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })])
|
|
])
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
}, openSharedMedia: {
|
|
if let controller = peerSharedMediaController(account: account, peerId: peerId) {
|
|
pushControllerImpl?(controller)
|
|
}
|
|
}, openGroupsInCommon: {
|
|
pushControllerImpl?(groupsInCommonController(account: account, peerId: peerId))
|
|
}, updatePeerBlocked: { value in
|
|
updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start())
|
|
}, deleteContact: {
|
|
|
|
})
|
|
|
|
let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)]))
|
|
|> map { state, view, chatState -> (ItemListControllerState, (ItemListNodeState<UserInfoEntry>, UserInfoEntry.ItemGenerationArguments)) in
|
|
let peer = peerViewMainPeer(view)
|
|
var leftNavigationButton: ItemListNavigationButton?
|
|
let rightNavigationButton: ItemListNavigationButton
|
|
if let editingState = state.editingState {
|
|
leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: {
|
|
updateState {
|
|
$0.withUpdatedEditingState(nil)
|
|
}
|
|
})
|
|
|
|
var doneEnabled = true
|
|
if let editingName = editingState.editingName, editingName.isEmpty {
|
|
doneEnabled = false
|
|
}
|
|
|
|
if state.savingData {
|
|
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {})
|
|
} else {
|
|
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: {
|
|
var updateName: ItemListAvatarAndNameInfoItemName?
|
|
updateState { state in
|
|
if let editingState = state.editingState, let editingName = editingState.editingName {
|
|
if let user = peer {
|
|
if ItemListAvatarAndNameInfoItemName(user.indexName) != editingName {
|
|
updateName = editingName
|
|
}
|
|
}
|
|
}
|
|
if updateName != nil {
|
|
return state.withUpdatedSavingData(true)
|
|
} else {
|
|
return state.withUpdatedEditingState(nil)
|
|
}
|
|
}
|
|
|
|
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
|
|
updatePeerNameDisposable.set((updateContactName(account: account, peerId: peerId, firstName: firstName, lastName: lastName) |> deliverOnMainQueue).start(error: { _ in
|
|
updateState { state in
|
|
return state.withUpdatedSavingData(false)
|
|
}
|
|
}, completed: {
|
|
updateState { state in
|
|
return state.withUpdatedSavingData(false).withUpdatedEditingState(nil)
|
|
}
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
|
|
if let user = peer {
|
|
updateState { state in
|
|
return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user.indexName)))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
let controllerState = ItemListControllerState(title: "Info", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton)
|
|
let listState = ItemListNodeState(entries: userInfoEntries(account: account, view: view, state: state, peerChatState: (chatState.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState), style: .plain)
|
|
|
|
return (controllerState, (listState, arguments))
|
|
} |> afterDisposed {
|
|
actionsDisposable.dispose()
|
|
}
|
|
|
|
let controller = ItemListController(signal)
|
|
|
|
pushControllerImpl = { [weak controller] value in
|
|
(controller?.navigationController as? NavigationController)?.pushViewController(value)
|
|
}
|
|
presentControllerImpl = { [weak controller] value, presentationArguments in
|
|
controller?.present(value, in: .window, with: presentationArguments)
|
|
}
|
|
openChatImpl = { [weak controller] in
|
|
if let navigationController = (controller?.navigationController as? NavigationController) {
|
|
navigateToChatController(navigationController: navigationController, account: account, peerId: peerId)
|
|
}
|
|
}
|
|
return controller
|
|
}
|