mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-15 18:59:54 +00:00
no message
This commit is contained in:
parent
a68d6f31c1
commit
bcecb46e94
22
Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/Contents.json
vendored
Normal file
22
Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "GroupInfoIconAddMember@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "GroupInfoIconAddMember@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@2x.png
vendored
Normal file
BIN
Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 B |
BIN
Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@3x.png
vendored
Normal file
BIN
Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 B |
@ -32,8 +32,14 @@
|
||||
D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */; };
|
||||
D0B7F8E21D8A18070045D939 /* PeerMediaCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */; };
|
||||
D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */; };
|
||||
D0B843901DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8438F1DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift */; };
|
||||
D0B843921DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843911DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift */; };
|
||||
D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */; };
|
||||
D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */; };
|
||||
D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */; };
|
||||
D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */; };
|
||||
D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */; };
|
||||
D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D81DAAAA0C005F29E1 /* PeerInfoPeerItem.swift */; };
|
||||
D0B843DB1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift */; };
|
||||
D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; };
|
||||
D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; };
|
||||
D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; };
|
||||
@ -65,7 +71,6 @@
|
||||
D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1BE1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift */; };
|
||||
D0E7A1C11D8C258D00C37A6F /* ChatHistoryEntriesForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1C01D8C258D00C37A6F /* ChatHistoryEntriesForView.swift */; };
|
||||
D0E7A1C31D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1C21D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift */; };
|
||||
D0EE97161D88BB53006C18E1 /* UserInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EE97151D88BB53006C18E1 /* UserInfoController.swift */; };
|
||||
D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EE97191D88BCA0006C18E1 /* ChatInfo.swift */; };
|
||||
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */; };
|
||||
D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */; };
|
||||
@ -239,8 +244,14 @@
|
||||
D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = "<group>"; };
|
||||
D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionController.swift; sourceTree = "<group>"; };
|
||||
D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionControllerNode.swift; sourceTree = "<group>"; };
|
||||
D0B8438F1DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBroadcastInfoController.swift; sourceTree = "<group>"; };
|
||||
D0B843911DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoDisclosureItem.swift; sourceTree = "<group>"; };
|
||||
D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoController.swift; sourceTree = "<group>"; };
|
||||
D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D81DAAAA0C005F29E1 /* PeerInfoPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoPeerItem.swift; sourceTree = "<group>"; };
|
||||
D0B843DA1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoPeerActionItem.swift; sourceTree = "<group>"; };
|
||||
D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = "<group>"; };
|
||||
D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = "<group>"; };
|
||||
D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = "<group>"; };
|
||||
@ -272,7 +283,6 @@
|
||||
D0E7A1BE1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryViewForLocation.swift; sourceTree = "<group>"; };
|
||||
D0E7A1C01D8C258D00C37A6F /* ChatHistoryEntriesForView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryEntriesForView.swift; sourceTree = "<group>"; };
|
||||
D0E7A1C21D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedChatHistoryViewTransition.swift; sourceTree = "<group>"; };
|
||||
D0EE97151D88BB53006C18E1 /* UserInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoController.swift; sourceTree = "<group>"; };
|
||||
D0EE97191D88BCA0006C18E1 /* ChatInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInfo.swift; sourceTree = "<group>"; };
|
||||
D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = "<group>"; };
|
||||
D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = "<group>"; };
|
||||
@ -450,6 +460,8 @@
|
||||
D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */,
|
||||
D03120F51DA534C1006A2A60 /* PeerInfoActionItem.swift */,
|
||||
D0B843911DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift */,
|
||||
D0B843D81DAAAA0C005F29E1 /* PeerInfoPeerItem.swift */,
|
||||
D0B843DA1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift */,
|
||||
);
|
||||
name = Components;
|
||||
sourceTree = "<group>";
|
||||
@ -633,18 +645,21 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D003702C1DA43006004308D3 /* Components */,
|
||||
D0EE97141D88BB39006C18E1 /* User */,
|
||||
D0EE97141D88BB39006C18E1 /* Controller */,
|
||||
);
|
||||
name = "Peer Info";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0EE97141D88BB39006C18E1 /* User */ = {
|
||||
D0EE97141D88BB39006C18E1 /* Controller */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0EE97151D88BB53006C18E1 /* UserInfoController.swift */,
|
||||
D0B8438F1DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift */,
|
||||
D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */,
|
||||
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */,
|
||||
D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */,
|
||||
D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */,
|
||||
D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */,
|
||||
);
|
||||
name = User;
|
||||
name = Controller;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0F69CCE1D6B87950046BCD6 /* Files */ = {
|
||||
@ -1190,6 +1205,7 @@
|
||||
D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */,
|
||||
D0DE77001D92F1EB002B8809 /* ChatTitleView.swift in Sources */,
|
||||
D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */,
|
||||
D0B843DB1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift in Sources */,
|
||||
D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */,
|
||||
D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */,
|
||||
D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */,
|
||||
@ -1219,6 +1235,7 @@
|
||||
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */,
|
||||
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */,
|
||||
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */,
|
||||
D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */,
|
||||
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */,
|
||||
D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */,
|
||||
D0B843921DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift in Sources */,
|
||||
@ -1228,6 +1245,7 @@
|
||||
D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */,
|
||||
D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */,
|
||||
D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */,
|
||||
D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */,
|
||||
D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */,
|
||||
D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */,
|
||||
D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */,
|
||||
@ -1284,7 +1302,7 @@
|
||||
D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */,
|
||||
D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */,
|
||||
D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */,
|
||||
D0EE97161D88BB53006C18E1 /* UserInfoController.swift in Sources */,
|
||||
D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */,
|
||||
D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */,
|
||||
D0DE77271D932627002B8809 /* ChatHistoryNode.swift in Sources */,
|
||||
D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */,
|
||||
@ -1299,8 +1317,11 @@
|
||||
D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */,
|
||||
D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */,
|
||||
D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */,
|
||||
D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */,
|
||||
D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */,
|
||||
D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */,
|
||||
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */,
|
||||
D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */,
|
||||
D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */,
|
||||
D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */,
|
||||
D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */,
|
||||
@ -1324,7 +1345,6 @@
|
||||
D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */,
|
||||
D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */,
|
||||
D0F69DC91D6B89EB0046BCD6 /* ImageNode.swift in Sources */,
|
||||
D0B843901DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift in Sources */,
|
||||
D0DE77251D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */,
|
||||
D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */,
|
||||
D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */,
|
||||
|
||||
@ -1,333 +0,0 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
private enum ChannelInfoSection: UInt32 {
|
||||
case info
|
||||
case sharedMediaAndNotifications
|
||||
case reportOrLeave
|
||||
}
|
||||
|
||||
private enum ChannelInfoEntry: Comparable, Identifiable {
|
||||
case info(peer: Peer?, cachedData: CachedPeerData?)
|
||||
case about(text: String)
|
||||
case userName(value: String)
|
||||
case sharedMedia
|
||||
case notifications(settings: PeerNotificationSettings?)
|
||||
case report
|
||||
case leave
|
||||
|
||||
fileprivate var section: ChannelInfoSection {
|
||||
switch self {
|
||||
case .info, .about, .userName:
|
||||
return .info
|
||||
case .sharedMedia, .notifications:
|
||||
return .sharedMediaAndNotifications
|
||||
case .report, .leave:
|
||||
return .reportOrLeave
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var stableId: Int {
|
||||
return self.sortIndex
|
||||
}
|
||||
|
||||
fileprivate static func ==(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .info(lhsPeer, lhsCachedData):
|
||||
switch rhs {
|
||||
case let .info(rhsPeer, rhsCachedData):
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer == nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
|
||||
if !lhsCachedData.isEqual(to: rhsCachedData) {
|
||||
return false
|
||||
}
|
||||
} else if (rhsCachedData == nil) != (rhsCachedData != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .about(lhsText):
|
||||
switch rhs {
|
||||
case let .about(lhsText):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .userName(value):
|
||||
switch rhs {
|
||||
case .userName(value):
|
||||
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 .report:
|
||||
switch rhs {
|
||||
case .report:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .leave:
|
||||
switch rhs {
|
||||
case .leave:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sortIndex: Int {
|
||||
switch self {
|
||||
case .info:
|
||||
return 0
|
||||
case .about:
|
||||
return 1
|
||||
case .userName:
|
||||
return 1000
|
||||
case .sharedMedia:
|
||||
return 1004
|
||||
case .notifications:
|
||||
return 1005
|
||||
case .report:
|
||||
return 1006
|
||||
case .leave:
|
||||
return 1007
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate static func <(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool {
|
||||
return lhs.sortIndex < rhs.sortIndex
|
||||
}
|
||||
}
|
||||
|
||||
private func channelBroadcastInfoEntries(account: Account, peerId: PeerId) -> Signal<[ChannelInfoEntry], NoError> {
|
||||
return account.viewTracker.peerView(peerId)
|
||||
|> map { view -> [ChannelInfoEntry] in
|
||||
var entries: [ChannelInfoEntry] = []
|
||||
entries.append(.info(peer: view.peers[peerId], cachedData: view.cachedData))
|
||||
if let cachedChannelData = view.cachedData as? CachedChannelData {
|
||||
if let about = cachedChannelData.about, !about.isEmpty {
|
||||
entries.append(.about(text: about))
|
||||
}
|
||||
}
|
||||
if let channel = view.peers[peerId] as? TelegramChannel {
|
||||
if let username = channel.username, !username.isEmpty {
|
||||
entries.append(.userName(value: username))
|
||||
}
|
||||
entries.append(.sharedMedia)
|
||||
entries.append(.notifications(settings: view.notificationSettings))
|
||||
entries.append(.report)
|
||||
if channel.participationStatus == .member {
|
||||
entries.append(.leave)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
private struct ChannelInfoEntryTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private func infoItemForEntry(account: Account, entry: ChannelInfoEntry, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||
switch entry {
|
||||
case let .info(peer, cachedData):
|
||||
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: entry.section.rawValue)
|
||||
case let .about(text):
|
||||
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: entry.section.rawValue)
|
||||
case let .userName(value):
|
||||
return PeerInfoTextWithLabelItem(label: "share link", text: "https://telegram.me/\(value)", multiline: false, sectionId: entry.section.rawValue)
|
||||
return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
case .sharedMedia:
|
||||
return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: entry.section.rawValue, action: {
|
||||
interaction.openSharedMedia()
|
||||
})
|
||||
case let .notifications(settings):
|
||||
let label: String
|
||||
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
|
||||
label = "Disabled"
|
||||
} else {
|
||||
label = "Enabled"
|
||||
}
|
||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: entry.section.rawValue, action: {
|
||||
interaction.changeNotificationNoteSettings()
|
||||
})
|
||||
case .report:
|
||||
return PeerInfoActionItem(title: "Report", kind: .generic, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
case .leave:
|
||||
return PeerInfoActionItem(title: "Leave Channel", kind: .destructive, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedUserInfoEntryTransition(account: Account, from fromEntries: [ChannelInfoEntry], to toEntries: [ChannelInfoEntry], interaction: PeerInfoControllerInteraction) -> ChannelInfoEntryTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return ChannelInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
public class ChannelBroadcastInfoController: ListController {
|
||||
private let account: Account
|
||||
private let peerId: PeerId
|
||||
|
||||
private var _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
private let transitionDisposable = MetaDisposable()
|
||||
private let changeSettingsDisposable = MetaDisposable()
|
||||
|
||||
public init(account: Account, peerId: PeerId) {
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
|
||||
super.init()
|
||||
|
||||
self.title = "Info"
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.transitionDisposable.dispose()
|
||||
self.changeSettingsDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func displayNodeDidLoad() {
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
}, changeNotificationNoteSettings: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = ActionSheetController()
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
let notificationAction: (Int32) -> Void = { [weak strongSelf] muteUntil in
|
||||
if let strongSelf = strongSelf {
|
||||
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)
|
||||
}
|
||||
strongSelf.changeSettingsDisposable.set(changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.appDefault)).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() })])
|
||||
])
|
||||
strongSelf.present(controller, in: .window)
|
||||
}
|
||||
})
|
||||
|
||||
self.listDisplayNode.backgroundColor = UIColor.white
|
||||
|
||||
let previousEntries = Atomic<[ChannelInfoEntry]?>(value: nil)
|
||||
|
||||
let account = self.account
|
||||
let transition = channelBroadcastInfoEntries(account: self.account, peerId: self.peerId)
|
||||
|> map { entries -> (ChannelInfoEntryTransition, Bool, Bool) in
|
||||
let previous = previousEntries.swap(entries)
|
||||
return (preparedUserInfoEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), previous == nil, previous != nil)
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|
||||
self.transitionDisposable.set(transition.start(next: { [weak self] (transition, firstTime, animated) in
|
||||
self?.enqueueTransition(transition, firstTime: firstTime, animated: animated)
|
||||
}))
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: ChannelInfoEntryTransition, firstTime: Bool, animated: Bool) {
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
} else if animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
self.listDisplayNode.listView.deleteAndInsertItems(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(.single(true))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
210
TelegramUI/ChannelInfoEntries.swift
Normal file
210
TelegramUI/ChannelInfoEntries.swift
Normal file
@ -0,0 +1,210 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
private enum ChannelInfoSection: UInt32, PeerInfoSection {
|
||||
case info
|
||||
case sharedMediaAndNotifications
|
||||
case reportOrLeave
|
||||
|
||||
func isEqual(to: PeerInfoSection) -> Bool {
|
||||
guard let section = to as? ChannelInfoSection else {
|
||||
return false
|
||||
}
|
||||
return section == self
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ section: PeerInfoSection) -> Bool {
|
||||
guard let section = section as? ChannelInfoSection else {
|
||||
return false
|
||||
}
|
||||
return self.rawValue < section.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum ChannelInfoEntry: PeerInfoEntry {
|
||||
case info(peer: Peer?, cachedData: CachedPeerData?)
|
||||
case about(text: String)
|
||||
case userName(value: String)
|
||||
case sharedMedia
|
||||
case notifications(settings: PeerNotificationSettings?)
|
||||
case report
|
||||
case leave
|
||||
|
||||
var section: PeerInfoSection {
|
||||
switch self {
|
||||
case .info, .about, .userName:
|
||||
return ChannelInfoSection.info
|
||||
case .sharedMedia, .notifications:
|
||||
return ChannelInfoSection.sharedMediaAndNotifications
|
||||
case .report, .leave:
|
||||
return ChannelInfoSection.reportOrLeave
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int {
|
||||
return self.sortIndex
|
||||
}
|
||||
|
||||
func isEqual(to: PeerInfoEntry) -> Bool {
|
||||
guard let entry = to as? ChannelInfoEntry else {
|
||||
return false
|
||||
}
|
||||
switch self {
|
||||
case let .info(lhsPeer, lhsCachedData):
|
||||
switch entry {
|
||||
case let .info(rhsPeer, rhsCachedData):
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer == nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
|
||||
if !lhsCachedData.isEqual(to: rhsCachedData) {
|
||||
return false
|
||||
}
|
||||
} else if (rhsCachedData == nil) != (rhsCachedData != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .about(lhsText):
|
||||
switch entry {
|
||||
case let .about(lhsText):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .userName(value):
|
||||
switch entry {
|
||||
case .userName(value):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .sharedMedia:
|
||||
switch entry {
|
||||
case .sharedMedia:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .notifications(lhsSettings):
|
||||
switch entry {
|
||||
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 .report:
|
||||
switch entry {
|
||||
case .report:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .leave:
|
||||
switch entry {
|
||||
case .leave:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sortIndex: Int {
|
||||
switch self {
|
||||
case .info:
|
||||
return 0
|
||||
case .about:
|
||||
return 1
|
||||
case .userName:
|
||||
return 1000
|
||||
case .sharedMedia:
|
||||
return 1004
|
||||
case .notifications:
|
||||
return 1005
|
||||
case .report:
|
||||
return 1006
|
||||
case .leave:
|
||||
return 1007
|
||||
}
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool {
|
||||
guard let entry = entry as? ChannelInfoEntry else {
|
||||
return false
|
||||
}
|
||||
return self.sortIndex < entry.sortIndex
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .info(peer, cachedData):
|
||||
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain)
|
||||
case let .about(text):
|
||||
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue)
|
||||
case let .userName(value):
|
||||
return PeerInfoTextWithLabelItem(label: "share link", text: "https://telegram.me/\(value)", multiline: false, sectionId: self.section.rawValue)
|
||||
return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
case .sharedMedia:
|
||||
return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .plain, action: {
|
||||
interaction.openSharedMedia()
|
||||
})
|
||||
case let .notifications(settings):
|
||||
let label: String
|
||||
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
|
||||
label = "Disabled"
|
||||
} else {
|
||||
label = "Enabled"
|
||||
}
|
||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
interaction.changeNotificationNoteSettings()
|
||||
})
|
||||
case .report:
|
||||
return PeerInfoActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
case .leave:
|
||||
return PeerInfoActionItem(title: "Leave Channel", kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func channelBroadcastInfoEntries(view: PeerView) -> [PeerInfoEntry] {
|
||||
var entries: [PeerInfoEntry] = []
|
||||
entries.append(ChannelInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData))
|
||||
if let cachedChannelData = view.cachedData as? CachedChannelData {
|
||||
if let about = cachedChannelData.about, !about.isEmpty {
|
||||
entries.append(ChannelInfoEntry.about(text: about))
|
||||
}
|
||||
}
|
||||
if let channel = view.peers[view.peerId] as? TelegramChannel {
|
||||
if let username = channel.username, !username.isEmpty {
|
||||
entries.append(ChannelInfoEntry.userName(value: username))
|
||||
}
|
||||
entries.append(ChannelInfoEntry.sharedMedia)
|
||||
entries.append(ChannelInfoEntry.notifications(settings: view.notificationSettings))
|
||||
entries.append(ChannelInfoEntry.report)
|
||||
if channel.participationStatus == .member {
|
||||
entries.append(ChannelInfoEntry.leave)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
@ -446,10 +446,9 @@ public class ChatController: ViewController {
|
||||
self.navigationActionDisposable.set((self.peerView.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
||||
if let strongSelf = self, let peer = peerView.peers[peerView.peerId] {
|
||||
if let chatInfoController = chatInfoController(account: strongSelf.account, peer: peer) {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController)
|
||||
}
|
||||
if let strongSelf = self, let _ = peerView.peers[peerView.peerId] {
|
||||
let chatInfoController = PeerInfoController(account: strongSelf.account, peerId: peerView.peerId)
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController)
|
||||
}
|
||||
}))
|
||||
break
|
||||
|
||||
@ -3,22 +3,6 @@ import Postbox
|
||||
import TelegramCore
|
||||
import Display
|
||||
|
||||
func chatInfoController(account: Account, peer: Peer) -> ViewController? {
|
||||
if let user = peer as? TelegramUser {
|
||||
return UserInfoController(account: account, peerId: peer.id)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
return ChannelBroadcastInfoController(account: account, peerId: peer.id)
|
||||
case .group:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return PeerMediaCollectionController(account: account, peerId: peer.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func peerSharedMediaController(account: Account, peerId: PeerId) -> ViewController? {
|
||||
return PeerMediaCollectionController(account: account, peerId: peerId)
|
||||
}
|
||||
|
||||
@ -30,12 +30,15 @@ func ==(lhs: ChatListMessageViewPosition, rhs: ChatListMessageViewPosition) -> B
|
||||
|
||||
private enum ChatListControllerEntryId: Hashable, CustomStringConvertible {
|
||||
case Search
|
||||
case Hole(Int64)
|
||||
case PeerId(Int64)
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case .Search:
|
||||
return 0
|
||||
case let .Hole(peerId):
|
||||
return peerId.hashValue
|
||||
case let .PeerId(peerId):
|
||||
return peerId.hashValue
|
||||
}
|
||||
@ -45,6 +48,8 @@ private enum ChatListControllerEntryId: Hashable, CustomStringConvertible {
|
||||
switch self {
|
||||
case .Search:
|
||||
return "search"
|
||||
case let .Hole(value):
|
||||
return "hole(\(value))"
|
||||
case let .PeerId(value):
|
||||
return "peerId(\(value))"
|
||||
}
|
||||
@ -64,6 +69,13 @@ private func ==(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Hole(lhsId):
|
||||
switch rhs {
|
||||
case .Hole(lhsId):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .PeerId(lhsId):
|
||||
switch rhs {
|
||||
case let .PeerId(rhsId):
|
||||
@ -97,8 +109,12 @@ private enum ChatListControllerEntry: Comparable, Identifiable {
|
||||
switch self {
|
||||
case .SearchEntry:
|
||||
return .Search
|
||||
default:
|
||||
return .PeerId(self.index.id.peerId.toInt64())
|
||||
case let .MessageEntry(message, _, _):
|
||||
return .PeerId(message.id.peerId.toInt64())
|
||||
case let .HoleEntry(hole):
|
||||
return .Hole(Int64(hole.index.id.id))
|
||||
case let .Nothing(index):
|
||||
return .PeerId(index.id.peerId.toInt64())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -302,7 +318,7 @@ public class ChatListController: ViewController {
|
||||
animated = false
|
||||
}
|
||||
|
||||
strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ?scrollPosition : nil, animated: animated)
|
||||
strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ? scrollPosition : nil, animated: animated)
|
||||
firstTime = false
|
||||
}
|
||||
}))
|
||||
|
||||
@ -3,6 +3,7 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
|
||||
class ChatListEmptyItem: ListViewItem {
|
||||
let selectable: Bool = false
|
||||
@ -18,6 +19,18 @@ class ChatListEmptyItem: ListViewItem {
|
||||
completion(node, {})
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
assert(node is ChatListEmptyItemNode)
|
||||
if let node = node as? ChatListEmptyItemNode {
|
||||
Queue.mainQueue().async {
|
||||
node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.updateItemPosition(first: previousItem == nil, last: nextItem == nil)
|
||||
|
||||
completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let separatorHeight = 1.0 / UIScreen.main.scale
|
||||
|
||||
@ -24,6 +24,7 @@ class ChatListHoleItem: ListViewItem {
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
assert(node is ChatListHoleItemNode)
|
||||
if let node = node as? ChatListHoleItemNode {
|
||||
Queue.mainQueue().async {
|
||||
let layout = node.asyncLayout()
|
||||
|
||||
@ -35,6 +35,7 @@ class ChatListItem: ListViewItem {
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
assert(node is ChatListItemNode)
|
||||
if let node = node as? ChatListItemNode {
|
||||
Queue.mainQueue().async {
|
||||
node.setupItem(account: self.account, message: self.message, combinedReadState: self.combinedReadState, notificationSettings: self.notificationSettings)
|
||||
|
||||
@ -5,6 +5,7 @@ import TelegramCore
|
||||
|
||||
private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0)
|
||||
private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0)
|
||||
private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0)
|
||||
|
||||
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let textNode: TextNode
|
||||
@ -87,8 +88,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
switch entity.type {
|
||||
case .Url:
|
||||
string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
case .Email:
|
||||
string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
case .TextUrl:
|
||||
string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
case .Bold:
|
||||
string.addAttribute(NSFontAttributeName, value: messageBoldFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
case .Mention:
|
||||
string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
case .Code, .Pre:
|
||||
string.addAttribute(NSFontAttributeName, value: messageFixedFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound))
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
293
TelegramUI/GroupInfoEntries.swift
Normal file
293
TelegramUI/GroupInfoEntries.swift
Normal file
@ -0,0 +1,293 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed()
|
||||
|
||||
private enum GroupInfoSection: UInt32, PeerInfoSection {
|
||||
case info
|
||||
case about
|
||||
case sharedMediaAndNotifications
|
||||
case members
|
||||
case leave
|
||||
|
||||
func isEqual(to: PeerInfoSection) -> Bool {
|
||||
guard let section = to as? GroupInfoSection else {
|
||||
return false
|
||||
}
|
||||
return section == self
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ section: PeerInfoSection) -> Bool {
|
||||
guard let section = section as? GroupInfoSection else {
|
||||
return false
|
||||
}
|
||||
return self.rawValue < section.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum GroupInfoEntry: PeerInfoEntry {
|
||||
case info(peer: Peer?, cachedData: CachedPeerData?)
|
||||
case setGroupPhoto
|
||||
case aboutHeader
|
||||
case about(text: String)
|
||||
case sharedMedia
|
||||
case notifications(settings: PeerNotificationSettings?)
|
||||
case usersHeader
|
||||
case addMember
|
||||
case member(index: Int, peer: Peer?)
|
||||
case leave
|
||||
|
||||
var section: PeerInfoSection {
|
||||
switch self {
|
||||
case .info, .setGroupPhoto:
|
||||
return GroupInfoSection.info
|
||||
case .aboutHeader, .about:
|
||||
return GroupInfoSection.about
|
||||
case .sharedMedia, .notifications:
|
||||
return GroupInfoSection.sharedMediaAndNotifications
|
||||
case .usersHeader, .addMember, .member:
|
||||
return GroupInfoSection.members
|
||||
case .leave:
|
||||
return GroupInfoSection.leave
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to: PeerInfoEntry) -> Bool {
|
||||
guard let entry = to as? GroupInfoEntry else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch self {
|
||||
case let .info(lhsPeer, lhsCachedData):
|
||||
switch entry {
|
||||
case let .info(rhsPeer, rhsCachedData):
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer == nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
|
||||
if !lhsCachedData.isEqual(to: rhsCachedData) {
|
||||
return false
|
||||
}
|
||||
} else if (rhsCachedData == nil) != (rhsCachedData != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .setGroupPhoto:
|
||||
if case .setGroupPhoto = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .aboutHeader:
|
||||
if case .aboutHeader = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .about(lhsText):
|
||||
switch entry {
|
||||
case let .about(lhsText):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .sharedMedia:
|
||||
switch entry {
|
||||
case .sharedMedia:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .notifications(lhsSettings):
|
||||
switch entry {
|
||||
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 .usersHeader:
|
||||
if case .usersHeader = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .addMember:
|
||||
if case .addMember = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .member(lhsIndex, lhsPeer):
|
||||
if case let .member(rhsIndex, rhsPeer) = entry {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer != nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .leave:
|
||||
if case .leave = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int {
|
||||
return self.sortIndex
|
||||
}
|
||||
|
||||
private var sortIndex: Int {
|
||||
switch self {
|
||||
case .info:
|
||||
return 0
|
||||
case .setGroupPhoto:
|
||||
return 1
|
||||
case .aboutHeader:
|
||||
return 2
|
||||
case .about:
|
||||
return 3
|
||||
case .notifications:
|
||||
return 4
|
||||
case .sharedMedia:
|
||||
return 5
|
||||
case .usersHeader:
|
||||
return 6
|
||||
case .addMember:
|
||||
return 7
|
||||
case let .member(index, _):
|
||||
return 10 + index
|
||||
case .leave:
|
||||
return 1000000
|
||||
}
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool {
|
||||
guard let other = entry as? GroupInfoEntry else {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.sortIndex < other.sortIndex
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .info(peer, cachedData):
|
||||
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .blocks)
|
||||
case .setGroupPhoto:
|
||||
return PeerInfoActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .blocks, action: {
|
||||
})
|
||||
case let .notifications(settings):
|
||||
return PeerInfoDisclosureItem(title: "Notifications", label: "Enabled", sectionId: self.section.rawValue, style: .blocks, action: {
|
||||
})
|
||||
case .sharedMedia:
|
||||
return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .blocks, action: {
|
||||
interaction.openSharedMedia()
|
||||
})
|
||||
case .addMember:
|
||||
return PeerInfoPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section.rawValue, action: {
|
||||
|
||||
})
|
||||
case let .member(_, peer):
|
||||
return PeerInfoPeerItem(account: account, peer: peer, sectionId: self.section.rawValue, action: {
|
||||
if let peer = peer {
|
||||
interaction.openPeerInfo(peer.id)
|
||||
}
|
||||
})
|
||||
case .leave:
|
||||
return PeerInfoActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section.rawValue, style: .blocks, action: {
|
||||
})
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] {
|
||||
var entries: [PeerInfoEntry] = []
|
||||
entries.append(GroupInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData))
|
||||
entries.append(GroupInfoEntry.setGroupPhoto)
|
||||
|
||||
entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings))
|
||||
entries.append(GroupInfoEntry.sharedMedia)
|
||||
|
||||
entries.append(GroupInfoEntry.addMember)
|
||||
|
||||
if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants {
|
||||
let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in
|
||||
switch lhs {
|
||||
case .creator:
|
||||
return false
|
||||
case let .admin(lhsId, _, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .admin(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
case let .member(lhsId, _, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .admin(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
for i in 0 ..< sortedParticipants.count {
|
||||
if let peer = view.peers[sortedParticipants[i].peerId] {
|
||||
entries.append(GroupInfoEntry.member(index: i, peer: peer))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let group = view.peers[view.peerId] as? TelegramGroup {
|
||||
if case .Member = group.membership {
|
||||
entries.append(GroupInfoEntry.leave)
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
@ -8,23 +8,32 @@ enum PeerInfoActionKind {
|
||||
case destructive
|
||||
}
|
||||
|
||||
enum PeerInfoActionAlignment {
|
||||
case natural
|
||||
case center
|
||||
}
|
||||
|
||||
class PeerInfoActionItem: ListViewItem, PeerInfoItem {
|
||||
let title: String
|
||||
let kind: PeerInfoActionKind
|
||||
let alignment: PeerInfoActionAlignment
|
||||
let sectionId: PeerInfoItemSectionId
|
||||
let style: PeerInfoListStyle
|
||||
let action: () -> Void
|
||||
|
||||
init(title: String, kind: PeerInfoActionKind, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) {
|
||||
init(title: String, kind: PeerInfoActionKind, alignment: PeerInfoActionAlignment, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.kind = kind
|
||||
self.alignment = alignment
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
|
||||
async {
|
||||
let node = PeerInfoActionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
@ -41,7 +50,7 @@ class PeerInfoActionItem: ListViewItem, PeerInfoItem {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply()
|
||||
@ -63,11 +72,26 @@ class PeerInfoActionItem: ListViewItem, PeerInfoItem {
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class PeerInfoActionItemNode: ListViewItemNode {
|
||||
let titleNode: TextNode
|
||||
let separatorNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
let titleNode: TextNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
self.titleNode.contentMode = .left
|
||||
@ -77,35 +101,106 @@ class PeerInfoActionItemNode: ListViewItemNode {
|
||||
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
self.separatorNode.displaysAsynchronously = false
|
||||
self.separatorNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerInfoActionItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: PeerInfoActionItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
return { item, width, insets in
|
||||
return { item, width, neighbors in
|
||||
let sectionInset: CGFloat = 22.0
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.kind == .destructive ? UIColor(0xff3b30) : UIColor(0x1195f2)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
let contentSize = CGSize(width: width, height: 44.0)
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
contentSize = CGSize(width: width, height: 44.0)
|
||||
insets = peerInfoItemNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
contentSize = CGSize(width: width, height: 44.0)
|
||||
let topInset: CGFloat
|
||||
switch neighbors.top {
|
||||
case .sameSection, .none:
|
||||
topInset = 0.0
|
||||
case .otherSection:
|
||||
topInset = separatorHeight + 35.0
|
||||
}
|
||||
let bottomInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection, .otherSection:
|
||||
bottomInset = 0.0
|
||||
case .none:
|
||||
bottomInset = separatorHeight + 35.0
|
||||
}
|
||||
insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0)
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let _ = titleApply()
|
||||
|
||||
let leftInset: CGFloat = 35.0
|
||||
let leftInset: CGFloat
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
|
||||
switch item.style {
|
||||
case .plain:
|
||||
leftInset = 35.0
|
||||
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
leftInset = 16.0
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection:
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
case .none, .otherSection:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection:
|
||||
bottomStripeInset = 16.0
|
||||
case .none, .otherSection:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel))
|
||||
switch item.alignment {
|
||||
case .natural:
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
|
||||
case .center:
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: 12.0), size: titleLayout.size)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
@ -119,7 +214,19 @@ class PeerInfoActionItemNode: ListViewItemNode {
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
|
||||
@ -10,18 +10,20 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem {
|
||||
let peer: Peer?
|
||||
let cachedData: CachedPeerData?
|
||||
let sectionId: PeerInfoItemSectionId
|
||||
let style: PeerInfoListStyle
|
||||
|
||||
init(account: Account, peer: Peer?, cachedData: CachedPeerData?, sectionId: PeerInfoItemSectionId) {
|
||||
init(account: Account, peer: Peer?, cachedData: CachedPeerData?, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle) {
|
||||
self.account = account
|
||||
self.peer = peer
|
||||
self.cachedData = cachedData
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
|
||||
async {
|
||||
let node = PeerInfoAvatarAndNameItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
@ -38,7 +40,7 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply()
|
||||
@ -60,12 +62,27 @@ private let nameFont = Font.medium(19.0)
|
||||
private let statusFont = Font.regular(15.0)
|
||||
|
||||
class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
||||
let avatarNode: AvatarNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
|
||||
let nameNode: TextNode
|
||||
let statusNode: TextNode
|
||||
private let avatarNode: AvatarNode
|
||||
private let nameNode: TextNode
|
||||
private let statusNode: TextNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.avatarNode = AvatarNode(font: Font.regular(20.0))
|
||||
|
||||
self.nameNode = TextNode()
|
||||
@ -85,11 +102,11 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
||||
self.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerInfoAvatarAndNameItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: PeerInfoAvatarAndNameItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let layoutNameNode = TextNode.asyncLayout(self.nameNode)
|
||||
let layoutStatusNode = TextNode.asyncLayout(self.statusNode)
|
||||
|
||||
return { item, width, insets in
|
||||
return { item, width, neighbors in
|
||||
let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: item.peer?.displayTitle ?? "", font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
let statusText: String
|
||||
@ -121,8 +138,72 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
||||
|
||||
let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 96.0), insets: insets), { [weak self] in
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
switch item.style {
|
||||
case .plain:
|
||||
contentSize = CGSize(width: width, height: 96.0)
|
||||
insets = peerInfoItemNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
contentSize = CGSize(width: width, height: 92.0)
|
||||
let topInset: CGFloat
|
||||
switch neighbors.top {
|
||||
case .sameSection, .none:
|
||||
topInset = 0.0
|
||||
case .otherSection:
|
||||
topInset = separatorHeight + 35.0
|
||||
}
|
||||
insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0)
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
switch item.style {
|
||||
case .plain:
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode != nil {
|
||||
strongSelf.bottomStripeNode.removeFromSupernode()
|
||||
}
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection:
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
case .none, .otherSection:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection:
|
||||
bottomStripeInset = 16.0
|
||||
case .none, .otherSection:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: width, height: layoutSize.height))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: layoutSize.height - insets.top - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
let _ = nameNodeApply()
|
||||
let _ = statusNodeApply()
|
||||
|
||||
@ -133,7 +214,6 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode {
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 15.0, y: 15.0), size: CGSize(width: 66.0, height: 66.0))
|
||||
strongSelf.nameNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size)
|
||||
|
||||
|
||||
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0 + nameNodeLayout.size.height + 4.0), size: statusNodeLayout.size)
|
||||
}
|
||||
})
|
||||
|
||||
197
TelegramUI/PeerInfoController.swift
Normal file
197
TelegramUI/PeerInfoController.swift
Normal file
@ -0,0 +1,197 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
final class PeerInfoControllerInteraction {
|
||||
let openSharedMedia: () -> Void
|
||||
let changeNotificationNoteSettings: () -> Void
|
||||
let openPeerInfo: (PeerId) -> Void
|
||||
|
||||
init(openSharedMedia: @escaping () -> Void, changeNotificationNoteSettings: @escaping () -> Void, openPeerInfo: @escaping (PeerId) -> Void) {
|
||||
self.openSharedMedia = openSharedMedia
|
||||
self.changeNotificationNoteSettings = changeNotificationNoteSettings
|
||||
self.openPeerInfo = openPeerInfo
|
||||
}
|
||||
}
|
||||
|
||||
private struct PeerInfoSortableEntry: Identifiable, Comparable {
|
||||
let entry: PeerInfoEntry
|
||||
|
||||
var stableId: Int {
|
||||
return self.entry.stableId
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerInfoSortableEntry, rhs: PeerInfoSortableEntry) -> Bool {
|
||||
return lhs.entry.isEqual(to: rhs.entry)
|
||||
}
|
||||
|
||||
static func <(lhs: PeerInfoSortableEntry, rhs: PeerInfoSortableEntry) -> Bool {
|
||||
return lhs.entry.isOrderedBefore(rhs.entry)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PeerInfoEntryTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private func preparedPeerInfoEntryTransition(account: Account, from fromEntries: [PeerInfoSortableEntry], to toEntries: [PeerInfoSortableEntry], interaction: PeerInfoControllerInteraction) -> PeerInfoEntryTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.entry.item(account: account, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.entry.item(account: account, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return PeerInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
public final class PeerInfoController: ListController {
|
||||
private let account: Account
|
||||
private let peerId: PeerId
|
||||
|
||||
private var _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
private let transitionDisposable = MetaDisposable()
|
||||
private let changeSettingsDisposable = MetaDisposable()
|
||||
|
||||
private var currentListStyle: PeerInfoListStyle = .plain
|
||||
|
||||
public init(account: Account, peerId: PeerId) {
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
|
||||
super.init()
|
||||
|
||||
self.title = "Info"
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.transitionDisposable.dispose()
|
||||
self.changeSettingsDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func displayNodeDidLoad() {
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
}, changeNotificationNoteSettings: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = ActionSheetController()
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
let notificationAction: (Int32) -> Void = { [weak strongSelf] muteUntil in
|
||||
if let strongSelf = strongSelf {
|
||||
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)
|
||||
}
|
||||
strongSelf.changeSettingsDisposable.set(changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.appDefault)).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() })])
|
||||
])
|
||||
strongSelf.present(controller, in: .window)
|
||||
}
|
||||
}, openPeerInfo: { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
let controller = PeerInfoController(account: strongSelf.account, peerId: peerId)
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
}
|
||||
})
|
||||
|
||||
self.listDisplayNode.backgroundColor = UIColor.white
|
||||
|
||||
let previousEntries = Atomic<[PeerInfoSortableEntry]?>(value: nil)
|
||||
|
||||
let account = self.account
|
||||
let transition = account.viewTracker.peerView(self.peerId)
|
||||
|> map { view -> (PeerInfoEntryTransition, PeerInfoListStyle, Bool, Bool) in
|
||||
let entries = peerInfoEntries(view: view).map { PeerInfoSortableEntry(entry: $0) }
|
||||
assert(entries == entries.sorted())
|
||||
let previous = previousEntries.swap(entries)
|
||||
let style: PeerInfoListStyle
|
||||
if let group = view.peers[view.peerId] as? TelegramGroup {
|
||||
style = .blocks
|
||||
} else {
|
||||
style = .plain
|
||||
}
|
||||
return (preparedPeerInfoEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), style, previous == nil, previous != nil)
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|
||||
self.transitionDisposable.set(transition.start(next: { [weak self] (transition, style, firstTime, animated) in
|
||||
self?.enqueueTransition(transition, style: style, firstTime: firstTime, animated: animated)
|
||||
}))
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: PeerInfoEntryTransition, style: PeerInfoListStyle, firstTime: Bool, animated: Bool) {
|
||||
if self.currentListStyle != style {
|
||||
self.currentListStyle = style
|
||||
switch style {
|
||||
case .plain:
|
||||
self.listDisplayNode.backgroundColor = .white
|
||||
case .blocks:
|
||||
self.listDisplayNode.backgroundColor = UIColor(0xefeff4)
|
||||
}
|
||||
}
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
} else if animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
self.listDisplayNode.listView.deleteAndInsertItems(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(.single(true))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -7,19 +7,21 @@ class PeerInfoDisclosureItem: ListViewItem, PeerInfoItem {
|
||||
let title: String
|
||||
let label: String
|
||||
let sectionId: PeerInfoItemSectionId
|
||||
let style: PeerInfoListStyle
|
||||
let action: () -> Void
|
||||
|
||||
init(title: String, label: String, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) {
|
||||
init(title: String, label: String, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
|
||||
async {
|
||||
let node = PeerInfoDisclosureItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
@ -36,7 +38,7 @@ class PeerInfoDisclosureItem: ListViewItem, PeerInfoItem {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply()
|
||||
@ -59,13 +61,28 @@ private let titleFont = Font.regular(17.0)
|
||||
private let arrowImage = UIImage(bundleImageName: "Peer Info/DisclosureArrow")?.precomposed()
|
||||
|
||||
class PeerInfoDisclosureItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
let arrowNode: ASImageNode
|
||||
let separatorNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
|
||||
@ -82,37 +99,98 @@ class PeerInfoDisclosureItemNode: ListViewItemNode {
|
||||
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
self.separatorNode.displaysAsynchronously = false
|
||||
self.separatorNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.arrowNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerInfoDisclosureItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: PeerInfoDisclosureItem, _ width: CGFloat, _ insets: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
|
||||
return { item, width, insets in
|
||||
return { item, width, neighbors in
|
||||
let sectionInset: CGFloat = 22.0
|
||||
let rightInset: CGFloat = 34.0
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
contentSize = CGSize(width: width, height: 44.0)
|
||||
insets = peerInfoItemNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
contentSize = CGSize(width: width, height: 44.0)
|
||||
let topInset: CGFloat
|
||||
switch neighbors.top {
|
||||
case .sameSection, .none:
|
||||
topInset = 0.0
|
||||
case .otherSection:
|
||||
topInset = separatorHeight + 35.0
|
||||
}
|
||||
insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0)
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: UIColor(0x8e8e93)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
let contentSize = CGSize(width: width, height: 44.0)
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let _ = titleApply()
|
||||
let _ = labelApply()
|
||||
|
||||
let leftInset: CGFloat = 35.0
|
||||
let leftInset: CGFloat
|
||||
|
||||
switch item.style {
|
||||
case .plain:
|
||||
leftInset = 35.0
|
||||
|
||||
if strongSelf.backgroundNode.supernode != nil {
|
||||
strongSelf.backgroundNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode != nil {
|
||||
strongSelf.topStripeNode.removeFromSupernode()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight))
|
||||
case .blocks:
|
||||
leftInset = 16.0
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection:
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
case .none, .otherSection:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection:
|
||||
bottomStripeInset = 16.0
|
||||
case .none, .otherSection:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: layoutSize.height - insets.top - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
|
||||
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: 12.0), size: labelLayout.size)
|
||||
@ -121,8 +199,6 @@ class PeerInfoDisclosureItemNode: ListViewItemNode {
|
||||
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: 18.0), size: arrowImage.size)
|
||||
}
|
||||
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
@ -135,7 +211,19 @@ class PeerInfoDisclosureItemNode: ListViewItemNode {
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
|
||||
38
TelegramUI/PeerInfoEntries.swift
Normal file
38
TelegramUI/PeerInfoEntries.swift
Normal file
@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Display
|
||||
|
||||
protocol PeerInfoSection {
|
||||
var rawValue: UInt32 { get }
|
||||
func isEqual(to: PeerInfoSection) -> Bool
|
||||
func isOrderedBefore(_ section: PeerInfoSection) -> Bool
|
||||
}
|
||||
|
||||
protocol PeerInfoEntryStableId {
|
||||
func isEqual(to: PeerInfoEntryStableId) -> Bool
|
||||
}
|
||||
|
||||
protocol PeerInfoEntry {
|
||||
var section: PeerInfoSection { get }
|
||||
var stableId: Int { get }
|
||||
func isEqual(to: PeerInfoEntry) -> Bool
|
||||
func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool
|
||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem
|
||||
}
|
||||
|
||||
func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] {
|
||||
if let user = view.peers[view.peerId] as? TelegramUser {
|
||||
return userInfoEntries(view: view)
|
||||
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
return channelBroadcastInfoEntries(view: view)
|
||||
case .group:
|
||||
return []
|
||||
}
|
||||
} else if let group = view.peers[view.peerId] as? TelegramGroup {
|
||||
return groupInfoEntries(view: view)
|
||||
}
|
||||
return []
|
||||
}
|
||||
@ -5,14 +5,61 @@ protocol PeerInfoItem {
|
||||
var sectionId: PeerInfoItemSectionId { get }
|
||||
}
|
||||
|
||||
func peerInfoItemInsets(item: PeerInfoItem, topItem: PeerInfoItem?, bottomItem: PeerInfoItem?) -> UIEdgeInsets {
|
||||
var insets = UIEdgeInsets()
|
||||
if let topItem = topItem, topItem.sectionId != item.sectionId {
|
||||
insets.top += 22.0
|
||||
}
|
||||
if bottomItem == nil {
|
||||
insets.bottom += 22.0
|
||||
enum PeerInfoItemNeighbor {
|
||||
case none
|
||||
case otherSection
|
||||
case sameSection
|
||||
}
|
||||
|
||||
struct PeerInfoItemNeighbors {
|
||||
let top: PeerInfoItemNeighbor
|
||||
let bottom: PeerInfoItemNeighbor
|
||||
}
|
||||
|
||||
func peerInfoItemNeighbors(item: PeerInfoItem, topItem: PeerInfoItem?, bottomItem: PeerInfoItem?) -> PeerInfoItemNeighbors {
|
||||
let topNeighbor: PeerInfoItemNeighbor
|
||||
if let topItem = topItem {
|
||||
if topItem.sectionId != item.sectionId {
|
||||
topNeighbor = .otherSection
|
||||
} else {
|
||||
topNeighbor = .sameSection
|
||||
}
|
||||
} else {
|
||||
topNeighbor = .none
|
||||
}
|
||||
|
||||
let bottomNeighbor: PeerInfoItemNeighbor
|
||||
if let bottomItem = bottomItem {
|
||||
if bottomItem.sectionId != item.sectionId {
|
||||
bottomNeighbor = .otherSection
|
||||
} else {
|
||||
bottomNeighbor = .sameSection
|
||||
}
|
||||
} else {
|
||||
bottomNeighbor = .none
|
||||
}
|
||||
|
||||
return PeerInfoItemNeighbors(top: topNeighbor, bottom: bottomNeighbor)
|
||||
}
|
||||
|
||||
enum PeerInfoListStyle {
|
||||
case plain
|
||||
case blocks
|
||||
}
|
||||
|
||||
func peerInfoItemNeighborsPlainInsets(_ neighbors: PeerInfoItemNeighbors) -> UIEdgeInsets {
|
||||
var insets = UIEdgeInsets()
|
||||
switch neighbors.top {
|
||||
case .otherSection:
|
||||
insets.top += 22.0
|
||||
case .none, .sameSection:
|
||||
break
|
||||
}
|
||||
switch neighbors.bottom {
|
||||
case .none:
|
||||
insets.bottom += 22.0
|
||||
case .otherSection, .sameSection:
|
||||
break
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
207
TelegramUI/PeerInfoPeerActionItem.swift
Normal file
207
TelegramUI/PeerInfoPeerActionItem.swift
Normal file
@ -0,0 +1,207 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
class PeerInfoPeerActionItem: ListViewItem, PeerInfoItem {
|
||||
let icon: UIImage?
|
||||
let title: String
|
||||
let sectionId: PeerInfoItemSectionId
|
||||
let action: () -> Void
|
||||
|
||||
init(icon: UIImage?, title: String, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) {
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
|
||||
async {
|
||||
let node = PeerInfoPeerActionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
completion(node, {
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
if let node = node as? PeerInfoPeerActionItemNode {
|
||||
Queue.mainQueue().async {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
|
||||
func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class PeerInfoPeerActionItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerInfoPeerActionItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
return { item, width, neighbors in
|
||||
let leftInset: CGFloat = 65.0
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x1195f2)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
contentSize = CGSize(width: width, height: 44.0)
|
||||
let topInset: CGFloat
|
||||
switch neighbors.top {
|
||||
case .sameSection, .none:
|
||||
topInset = 0.0
|
||||
case .otherSection:
|
||||
topInset = separatorHeight + 35.0
|
||||
}
|
||||
insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let _ = titleApply()
|
||||
|
||||
strongSelf.iconNode.image = item.icon
|
||||
if let image = item.icon {
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection:
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
case .none, .otherSection:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection:
|
||||
bottomStripeInset = leftInset
|
||||
case .none, .otherSection:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
255
TelegramUI/PeerInfoPeerItem.swift
Normal file
255
TelegramUI/PeerInfoPeerItem.swift
Normal file
@ -0,0 +1,255 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
class PeerInfoPeerItem: ListViewItem, PeerInfoItem {
|
||||
let account: Account
|
||||
let peer: Peer?
|
||||
let sectionId: PeerInfoItemSectionId
|
||||
let action: () -> Void
|
||||
|
||||
init(account: Account, peer: Peer?, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.peer = peer
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
|
||||
async {
|
||||
let node = PeerInfoPeerItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
completion(node, {
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
if let node = node as? PeerInfoPeerItemNode {
|
||||
Queue.mainQueue().async {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
|
||||
func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
private let titleBoldFont = Font.medium(17.0)
|
||||
private let statusFont = Font.regular(14.0)
|
||||
private let avatarFont = Font.regular(17.0)
|
||||
|
||||
class PeerInfoPeerItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
private let avatarNode: AvatarNode
|
||||
private let titleNode: TextNode
|
||||
private let statusNode: TextNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.statusNode = TextNode()
|
||||
self.statusNode.isLayerBacked = true
|
||||
self.statusNode.contentMode = .left
|
||||
self.statusNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerInfoPeerItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
|
||||
|
||||
return { item, width, neighbors in
|
||||
var titleAttributedString: NSAttributedString?
|
||||
var statusAttributedString: NSAttributedString?
|
||||
|
||||
if let peer = item.peer {
|
||||
if let user = peer as? TelegramUser {
|
||||
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
|
||||
let string = NSMutableAttributedString()
|
||||
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black))
|
||||
string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black))
|
||||
string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black))
|
||||
titleAttributedString = string
|
||||
} else if let firstName = user.firstName, !firstName.isEmpty {
|
||||
titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black)
|
||||
} else if let lastName = user.lastName, !lastName.isEmpty {
|
||||
titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black)
|
||||
} else {
|
||||
titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6))
|
||||
}
|
||||
|
||||
statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6))
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black)
|
||||
}
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 65.0
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
contentSize = CGSize(width: width, height: 48.0)
|
||||
let topInset: CGFloat
|
||||
let bottomInset: CGFloat
|
||||
switch neighbors.top {
|
||||
case .sameSection, .none:
|
||||
topInset = 0.0
|
||||
case .otherSection:
|
||||
topInset = separatorHeight + 35.0
|
||||
}
|
||||
switch neighbors.bottom {
|
||||
case .none:
|
||||
bottomInset = separatorHeight + 35.0
|
||||
case .otherSection, .sameSection:
|
||||
bottomInset = separatorHeight
|
||||
}
|
||||
insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let _ = titleApply()
|
||||
let _ = statusApply()
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection:
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
case .none, .otherSection:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection:
|
||||
bottomStripeInset = leftInset
|
||||
case .none, .otherSection:
|
||||
bottomStripeInset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 5.0), size: titleLayout.size)
|
||||
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size)
|
||||
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))
|
||||
if let peer = item.peer {
|
||||
strongSelf.avatarNode.setPeer(account: item.account, peer: peer)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 48.0 + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,7 @@ final class PeerInfoTextWithLabelItem: ListViewItem, PeerInfoItem {
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) {
|
||||
async {
|
||||
let node = PeerInfoTextWithLabelItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
@ -36,7 +36,7 @@ final class PeerInfoTextWithLabelItem: ListViewItem, PeerInfoItem {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply()
|
||||
@ -85,11 +85,12 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode {
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerInfoTextWithLabelItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: PeerInfoTextWithLabelItem, _ width: CGFloat, _ insets: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
return { item, width, insets in
|
||||
return { item, width, neighbors in
|
||||
let insets = peerInfoItemNeighborsPlainInsets(neighbors)
|
||||
let leftInset: CGFloat = 35.0
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: UIColor(0x1195f2)), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
@ -1,386 +0,0 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
private enum UserInfoSection: UInt32 {
|
||||
case info
|
||||
case actions
|
||||
case sharedMediaAndNotifications
|
||||
case block
|
||||
}
|
||||
|
||||
private enum UserInfoEntry: Comparable, Identifiable {
|
||||
case info(peer: Peer?, cachedData: CachedPeerData?)
|
||||
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 block
|
||||
|
||||
fileprivate var section: UserInfoSection {
|
||||
switch self {
|
||||
case .info, .about, .phoneNumber, .userName:
|
||||
return .info
|
||||
case .sendMessage, .shareContact, .startSecretChat:
|
||||
return .actions
|
||||
case .sharedMedia, .notifications:
|
||||
return .sharedMediaAndNotifications
|
||||
case .block:
|
||||
return .block
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var stableId: Int {
|
||||
return self.sortIndex
|
||||
}
|
||||
|
||||
fileprivate static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .info(lhsPeer, lhsCachedData):
|
||||
switch rhs {
|
||||
case let .info(rhsPeer, rhsCachedData):
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer == nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
|
||||
if !lhsCachedData.isEqual(to: rhsCachedData) {
|
||||
return false
|
||||
}
|
||||
} else if (rhsCachedData == nil) != (rhsCachedData != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .about(lhsText):
|
||||
switch rhs {
|
||||
case let .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 .block:
|
||||
switch rhs {
|
||||
case .block:
|
||||
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 .block:
|
||||
return 1006
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate static func <(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool {
|
||||
return lhs.sortIndex < rhs.sortIndex
|
||||
}
|
||||
}
|
||||
|
||||
private func userInfoEntries(account: Account, peerId: PeerId) -> Signal<[UserInfoEntry], NoError> {
|
||||
return account.viewTracker.peerView(peerId)
|
||||
|> map { view -> [UserInfoEntry] in
|
||||
var entries: [UserInfoEntry] = []
|
||||
entries.append(.info(peer: view.peers[peerId], cachedData: view.cachedData))
|
||||
if let cachedUserData = view.cachedData as? CachedUserData {
|
||||
if let about = cachedUserData.about, !about.isEmpty {
|
||||
entries.append(.about(text: about))
|
||||
}
|
||||
}
|
||||
if let user = view.peers[peerId] as? TelegramUser {
|
||||
if let phoneNumber = user.phone, !phoneNumber.isEmpty {
|
||||
entries.append(.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber)))
|
||||
}
|
||||
if let username = user.username, !username.isEmpty {
|
||||
entries.append(.userName(value: username))
|
||||
}
|
||||
entries.append(.sendMessage)
|
||||
entries.append(.shareContact)
|
||||
entries.append(.startSecretChat)
|
||||
entries.append(.sharedMedia)
|
||||
entries.append(.notifications(settings: view.notificationSettings))
|
||||
entries.append(.block)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
private struct UserInfoEntryTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private func infoItemForEntry(account: Account, entry: UserInfoEntry, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||
switch entry {
|
||||
case let .info(peer, cachedData):
|
||||
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: entry.section.rawValue)
|
||||
case let .about(text):
|
||||
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: entry.section.rawValue)
|
||||
case let .phoneNumber(_, value):
|
||||
return PeerInfoTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: entry.section.rawValue)
|
||||
case let .userName(value):
|
||||
return PeerInfoTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: entry.section.rawValue)
|
||||
case .sendMessage:
|
||||
return PeerInfoActionItem(title: "Send Message", kind: .generic, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
case .shareContact:
|
||||
return PeerInfoActionItem(title: "Share Contact", kind: .generic, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
case .startSecretChat:
|
||||
return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
case .sharedMedia:
|
||||
return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: entry.section.rawValue, action: {
|
||||
interaction.openSharedMedia()
|
||||
})
|
||||
case let .notifications(settings):
|
||||
let label: String
|
||||
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
|
||||
label = "Disabled"
|
||||
} else {
|
||||
label = "Enabled"
|
||||
}
|
||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: entry.section.rawValue, action: {
|
||||
interaction.changeNotificationNoteSettings()
|
||||
})
|
||||
case .block:
|
||||
return PeerInfoActionItem(title: "Block User", kind: .destructive, sectionId: entry.section.rawValue, action: {
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedUserInfoEntryTransition(account: Account, from fromEntries: [UserInfoEntry], to toEntries: [UserInfoEntry], interaction: PeerInfoControllerInteraction) -> UserInfoEntryTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) }
|
||||
|
||||
return UserInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
final class PeerInfoControllerInteraction {
|
||||
let openSharedMedia: () -> Void
|
||||
let changeNotificationNoteSettings: () -> Void
|
||||
|
||||
init(openSharedMedia: @escaping () -> Void, changeNotificationNoteSettings: @escaping () -> Void) {
|
||||
self.openSharedMedia = openSharedMedia
|
||||
self.changeNotificationNoteSettings = changeNotificationNoteSettings
|
||||
}
|
||||
}
|
||||
|
||||
public class UserInfoController: ListController {
|
||||
private let account: Account
|
||||
private let peerId: PeerId
|
||||
|
||||
private var _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
private let transitionDisposable = MetaDisposable()
|
||||
private let changeSettingsDisposable = MetaDisposable()
|
||||
|
||||
public init(account: Account, peerId: PeerId) {
|
||||
self.account = account
|
||||
self.peerId = peerId
|
||||
|
||||
super.init()
|
||||
|
||||
self.title = "Info"
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.transitionDisposable.dispose()
|
||||
self.changeSettingsDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func displayNodeDidLoad() {
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
}, changeNotificationNoteSettings: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = ActionSheetController()
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
let notificationAction: (Int32) -> Void = { [weak strongSelf] muteUntil in
|
||||
if let strongSelf = strongSelf {
|
||||
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)
|
||||
}
|
||||
strongSelf.changeSettingsDisposable.set(changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.appDefault)).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() })])
|
||||
])
|
||||
strongSelf.present(controller, in: .window)
|
||||
}
|
||||
})
|
||||
|
||||
self.listDisplayNode.backgroundColor = UIColor.white
|
||||
|
||||
let previousEntries = Atomic<[UserInfoEntry]?>(value: nil)
|
||||
|
||||
let account = self.account
|
||||
let transition = userInfoEntries(account: self.account, peerId: self.peerId)
|
||||
|> map { entries -> (UserInfoEntryTransition, Bool, Bool) in
|
||||
let previous = previousEntries.swap(entries)
|
||||
return (preparedUserInfoEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), previous == nil, previous != nil)
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|
||||
self.transitionDisposable.set(transition.start(next: { [weak self] (transition, firstTime, animated) in
|
||||
self?.enqueueTransition(transition, firstTime: firstTime, animated: animated)
|
||||
}))
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: UserInfoEntryTransition, firstTime: Bool, animated: Bool) {
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
} else if animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
self.listDisplayNode.listView.deleteAndInsertItems(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(.single(true))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
255
TelegramUI/UserInfoEntries.swift
Normal file
255
TelegramUI/UserInfoEntries.swift
Normal file
@ -0,0 +1,255 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
private enum UserInfoSection: UInt32, PeerInfoSection {
|
||||
case info
|
||||
case actions
|
||||
case sharedMediaAndNotifications
|
||||
case block
|
||||
|
||||
func isEqual(to: PeerInfoSection) -> Bool {
|
||||
guard let section = to as? UserInfoSection else {
|
||||
return false
|
||||
}
|
||||
return section == self
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ section: PeerInfoSection) -> Bool {
|
||||
guard let section = section as? UserInfoSection else {
|
||||
return false
|
||||
}
|
||||
return self.rawValue < section.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
enum UserInfoEntry: PeerInfoEntry {
|
||||
case info(peer: Peer?, cachedData: CachedPeerData?)
|
||||
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 block
|
||||
|
||||
var section: PeerInfoSection {
|
||||
switch self {
|
||||
case .info, .about, .phoneNumber, .userName:
|
||||
return UserInfoSection.info
|
||||
case .sendMessage, .shareContact, .startSecretChat:
|
||||
return UserInfoSection.actions
|
||||
case .sharedMedia, .notifications:
|
||||
return UserInfoSection.sharedMediaAndNotifications
|
||||
case .block:
|
||||
return UserInfoSection.block
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int {
|
||||
return self.sortIndex
|
||||
}
|
||||
|
||||
func isEqual(to: PeerInfoEntry) -> Bool {
|
||||
guard let entry = to as? UserInfoEntry else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch self {
|
||||
case let .info(lhsPeer, lhsCachedData):
|
||||
switch entry {
|
||||
case let .info(rhsPeer, rhsCachedData):
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer == nil) != (rhsPeer != 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
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .about(lhsText):
|
||||
switch entry {
|
||||
case let .about(lhsText):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .phoneNumber(lhsIndex, lhsValue):
|
||||
switch entry {
|
||||
case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .userName(value):
|
||||
switch entry {
|
||||
case .userName(value):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .sendMessage:
|
||||
switch entry {
|
||||
case .sendMessage:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .shareContact:
|
||||
switch entry {
|
||||
case .shareContact:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .startSecretChat:
|
||||
switch entry {
|
||||
case .startSecretChat:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .sharedMedia:
|
||||
switch entry {
|
||||
case .sharedMedia:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .notifications(lhsSettings):
|
||||
switch entry {
|
||||
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 .block:
|
||||
switch entry {
|
||||
case .block:
|
||||
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 .block:
|
||||
return 1006
|
||||
}
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool {
|
||||
guard let other = entry as? UserInfoEntry else {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.sortIndex < other.sortIndex
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .info(peer, cachedData):
|
||||
return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain)
|
||||
case let .about(text):
|
||||
return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue)
|
||||
case let .phoneNumber(_, value):
|
||||
return PeerInfoTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section.rawValue)
|
||||
case let .userName(value):
|
||||
return PeerInfoTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section.rawValue)
|
||||
case .sendMessage:
|
||||
return PeerInfoActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
case .shareContact:
|
||||
return PeerInfoActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
case .startSecretChat:
|
||||
return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
case .sharedMedia:
|
||||
return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .plain, action: {
|
||||
interaction.openSharedMedia()
|
||||
})
|
||||
case let .notifications(settings):
|
||||
let label: String
|
||||
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
|
||||
label = "Disabled"
|
||||
} else {
|
||||
label = "Enabled"
|
||||
}
|
||||
return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
interaction.changeNotificationNoteSettings()
|
||||
})
|
||||
case .block:
|
||||
return PeerInfoActionItem(title: "Block User", kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: {
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userInfoEntries(view: PeerView) -> [PeerInfoEntry] {
|
||||
var entries: [PeerInfoEntry] = []
|
||||
entries.append(UserInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData))
|
||||
if let cachedUserData = view.cachedData as? CachedUserData {
|
||||
if let about = cachedUserData.about, !about.isEmpty {
|
||||
entries.append(UserInfoEntry.about(text: about))
|
||||
}
|
||||
}
|
||||
if let user = view.peers[view.peerId] as? TelegramUser {
|
||||
if let phoneNumber = user.phone, !phoneNumber.isEmpty {
|
||||
entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber)))
|
||||
}
|
||||
if let username = user.username, !username.isEmpty {
|
||||
entries.append(UserInfoEntry.userName(value: username))
|
||||
}
|
||||
entries.append(UserInfoEntry.sendMessage)
|
||||
entries.append(UserInfoEntry.shareContact)
|
||||
entries.append(UserInfoEntry.startSecretChat)
|
||||
entries.append(UserInfoEntry.sharedMedia)
|
||||
entries.append(UserInfoEntry.notifications(settings: view.notificationSettings))
|
||||
entries.append(UserInfoEntry.block)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user