Swiftgram/TelegramUI/PeopleNearbyController.swift
Ilya Laktyushin bc96107da1 Support of channel/group ownership transfer
People Nearby initial implementation
Ghost Icons for deleted users
2019-06-08 00:12:50 +02:00

329 lines
16 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MapKit
private struct PeerNearbyEntry {
let peer: Peer
let expires: Int32
let distance: Int32
}
private func arePeersNearbyEqual(_ lhs: PeerNearbyEntry?, _ rhs: PeerNearbyEntry?) -> Bool {
if let lhs = lhs, let rhs = rhs {
return lhs.peer.isEqual(rhs.peer) && lhs.expires == rhs.expires && lhs.distance == rhs.distance
} else {
return (lhs != nil) == (rhs != nil)
}
}
private func arePeerNearbyArraysEqual(_ lhs: [PeerNearbyEntry], _ rhs: [PeerNearbyEntry]) -> Bool {
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if !lhs[i].peer.isEqual(rhs[i].peer) || lhs[i].expires != rhs[i].expires || lhs[i].distance != rhs[i].distance {
return false
}
}
return true
}
private final class PeopleNearbyControllerArguments {
let context: AccountContext
let openChat: (Peer) -> Void
let openCreateGroup: () -> Void
init(context: AccountContext, openChat: @escaping (Peer) -> Void, openCreateGroup: @escaping () -> Void) {
self.context = context
self.openChat = openChat
self.openCreateGroup = openCreateGroup
}
}
private enum PeopleNearbySection: Int32 {
case header
case users
case groups
case channels
}
private enum PeopleNearbyEntry: ItemListNodeEntry {
case header(PresentationTheme, String)
case usersHeader(PresentationTheme, String)
case empty(PresentationTheme, String)
case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
case groupsHeader(PresentationTheme, String)
case createGroup(PresentationTheme, String)
case group(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
case channelsHeader(PresentationTheme, String)
case channel(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry)
var section: ItemListSectionId {
switch self {
case .header:
return PeopleNearbySection.header.rawValue
case .usersHeader, .empty, .user:
return PeopleNearbySection.users.rawValue
case .groupsHeader, .createGroup, .group:
return PeopleNearbySection.groups.rawValue
case .channelsHeader, .channel:
return PeopleNearbySection.channels.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .usersHeader:
return 1
case .empty:
return 2
case let .user(index, _, _, _, _, _):
return 3 + index
case .groupsHeader:
return 1000
case .createGroup:
return 1001
case let .group(index, _, _, _, _, _):
return 1002 + index
case .channelsHeader:
return 2000
case let .channel(index, _, _, _, _, _):
return 2001 + index
}
}
static func ==(lhs: PeopleNearbyEntry, rhs: PeopleNearbyEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsText):
if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .usersHeader(lhsTheme, lhsText):
if case let .usersHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .empty(lhsTheme, lhsText):
if case let .empty(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .user(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer):
if case let .user(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) {
return true
} else {
return false
}
case let .groupsHeader(lhsTheme, lhsText):
if case let .groupsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .createGroup(lhsTheme, lhsText):
if case let .createGroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .group(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer):
if case let .group(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) {
return true
} else {
return false
}
case let .channelsHeader(lhsTheme, lhsText):
if case let .channelsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channel(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer):
if case let .channel(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) {
return true
} else {
return false
}
}
}
static func <(lhs: PeopleNearbyEntry, rhs: PeopleNearbyEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: PeopleNearbyControllerArguments) -> ListViewItem {
switch self {
case let .header(theme, text):
return PeopleNearbyHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .usersHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .empty(theme, text):
return ItemListPlaceholderItem(theme: theme, text: text, sectionId: self.section, style: .blocks)
case let .user(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
func distance(_ distance: Int32) -> String {
let formatter = MKDistanceFormatter()
formatter.unitStyle = .abbreviated
return formatter.string(fromDistance: Double(distance))
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(distance(peer.distance)), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer.peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopStripe: false, hasTopGroupInset: false, tag: nil)
case let .groupsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .createGroup(theme, title):
return ContactListActionItem(theme: theme, title: title, icon: .generic(UIImage(bundleImageName: "Contact List/CreateGroupActionIcon")!), header: nil, action: {
arguments.openCreateGroup()
})
case let .group(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text("10 members"), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer.peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopStripe: false, hasTopGroupInset: false, tag: nil)
case let .channelsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .channel(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text("10 members"), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer.peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopStripe: false, hasTopGroupInset: false, tag: nil)
}
}
}
private struct PeopleNearbyControllerState: Equatable {
static func ==(lhs: PeopleNearbyControllerState, rhs: PeopleNearbyControllerState) -> Bool {
return true
}
}
private struct PeopleNearbyData: Equatable {
let users: [PeerNearbyEntry]
let groups: [PeerNearbyEntry]
let channels: [PeerNearbyEntry]
init(users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) {
self.users = users
self.groups = groups
self.channels = channels
}
static func ==(lhs: PeopleNearbyData, rhs: PeopleNearbyData) -> Bool {
return arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels)
}
}
private func peopleNearbyControllerEntries(state: PeopleNearbyControllerState, data: PeopleNearbyData?, presentationData: PresentationData) -> [PeopleNearbyEntry] {
var entries: [PeopleNearbyEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.PeopleNearby_Description))
entries.append(.usersHeader(presentationData.theme, presentationData.strings.PeopleNearby_Users.uppercased()))
if let data = data, !data.users.isEmpty {
var i: Int32 = 0
for user in data.users {
entries.append(.user(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, user))
i += 1
}
} else {
entries.append(.empty(presentationData.theme, presentationData.strings.PeopleNearby_UsersEmpty))
}
entries.append(.groupsHeader(presentationData.theme, presentationData.strings.PeopleNearby_Groups.uppercased()))
entries.append(.createGroup(presentationData.theme, presentationData.strings.PeopleNearby_CreateGroup))
if let data = data, !data.groups.isEmpty {
var i: Int32 = 0
for group in data.groups {
entries.append(.group(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, group))
i += 1
}
}
if let data = data, !data.channels.isEmpty {
var i: Int32 = 0
for channel in data.channels {
entries.append(.channel(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, channel))
i += 1
}
}
return entries
}
public func peopleNearbyController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(PeopleNearbyControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PeopleNearbyControllerState())
let updateState: ((PeopleNearbyControllerState) -> PeopleNearbyControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigateToChatImpl: ((Peer) -> Void)?
let actionsDisposable = DisposableSet()
let dataPromise = Promise<PeopleNearbyData?>(nil)
let arguments = PeopleNearbyControllerArguments(context: context, openChat: { peer in
navigateToChatImpl?(peer)
}, openCreateGroup: {
})
let dataSignal: Signal<PeopleNearbyData?, NoError> = currentLocationManagerCoordinate(manager: context.sharedContext.locationManager!, timeout: 5.0)
|> mapToSignal { coordinate -> Signal<PeopleNearbyData?, NoError> in
guard let coordinate = coordinate else {
return .single(nil)
}
return peersNearby(network: context.account.network, accountStateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude), radius: 200)
|> mapToSignal { peersNearby -> Signal<PeopleNearbyData?, NoError> in
return context.account.postbox.transaction { transaction -> PeopleNearbyData? in
var result: [PeerNearbyEntry] = []
for peerNearby in peersNearby {
if peerNearby.id != context.account.peerId, let peer = transaction.getPeer(peerNearby.id) {
result.append(PeerNearbyEntry(peer: peer, expires: peerNearby.expires, distance: peerNearby.distance))
}
}
return PeopleNearbyData(users: result, groups: [], channels: [])
}
}
}
dataPromise.set(dataSignal)
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), dataPromise.get())
|> deliverOnMainQueue
|> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState<PeopleNearbyEntry>, PeopleNearbyEntry.ItemGenerationArguments)) in
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PeopleNearby_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(entries: peopleNearbyControllerEntries(state: state, data: data, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: true, userInteractionEnabled: true)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
navigateToChatImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peer.id))
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
return controller
}