Various fixes

This commit is contained in:
Ilya Laktyushin 2024-12-12 20:41:33 +04:00
parent 9e908c9ea7
commit ad9f0d6847
14 changed files with 214 additions and 31 deletions

View File

@ -33,6 +33,7 @@ public struct AttachmentMainButtonState {
}
public let text: String?
public let badge: String?
public let font: Font
public let background: Background
public let textColor: UIColor
@ -44,6 +45,7 @@ public struct AttachmentMainButtonState {
public init(
text: String?,
badge: String? = nil,
font: Font,
background: Background,
textColor: UIColor,
@ -54,6 +56,7 @@ public struct AttachmentMainButtonState {
position: Position? = nil
) {
self.text = text
self.badge = badge
self.font = font
self.background = background
self.textColor = textColor

View File

@ -79,7 +79,7 @@ public enum ContactMultiselectionControllerMode {
case channelCreation
case chatSelection(ChatSelection)
case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool, hasActions: Bool)
case requestedUsersSelection
case requestedUsersSelection(isBot: Bool?, isPremium: Bool?)
}
public enum ContactListFilter {

View File

@ -59,6 +59,7 @@ public final class PeerSelectionControllerParams {
public let createNewGroup: (() -> Void)?
public let pretendPresentedInModal: Bool
public let multipleSelection: Bool
public let multipleSelectionLimit: Int32?
public let forwardedMessageIds: [EngineMessage.Id]
public let hasTypeHeaders: Bool
public let selectForumThreads: Bool
@ -80,6 +81,7 @@ public final class PeerSelectionControllerParams {
createNewGroup: (() -> Void)? = nil,
pretendPresentedInModal: Bool = false,
multipleSelection: Bool = false,
multipleSelectionLimit: Int32? = nil,
forwardedMessageIds: [EngineMessage.Id] = [],
hasTypeHeaders: Bool = false,
selectForumThreads: Bool = false,
@ -100,6 +102,7 @@ public final class PeerSelectionControllerParams {
self.createNewGroup = createNewGroup
self.pretendPresentedInModal = pretendPresentedInModal
self.multipleSelection = multipleSelection
self.multipleSelectionLimit = multipleSelectionLimit
self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders
self.selectForumThreads = selectForumThreads

View File

@ -382,12 +382,102 @@ private final class LoadingProgressNode: ASDisplayNode {
}
}
private final class BadgeNode: ASDisplayNode {
private var fillColor: UIColor
private var strokeColor: UIColor
private var textColor: UIColor
private let textNode: ImmediateTextNode
private let backgroundNode: ASImageNode
private let font: UIFont = Font.with(size: 15.0, design: .round, weight: .bold)
var text: String = "" {
didSet {
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
self.invalidateCalculatedLayout()
}
}
init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: nil, strokeWidth: 1.0)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.isUserInteractionEnabled = false
}
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
}
func animateBump(incremented: Bool) {
if incremented {
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2)
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in
if finished {
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
}
})
} else {
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8)
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in
if finished {
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
}
})
}
}
func animateOut() {
let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
}
func update(_ constrainedSize: CGSize) -> CGSize {
let badgeSize = self.textNode.updateLayout(constrainedSize)
let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 8.0), height: 18.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
self.backgroundNode.frame = backgroundFrame
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize)
return backgroundSize
}
}
private final class MainButtonNode: HighlightTrackingButtonNode {
private var state: AttachmentMainButtonState
private var size: CGSize?
private let backgroundAnimationNode: ASImageNode
fileprivate let textNode: ImmediateTextNode
private var badgeNode: BadgeNode?
private let statusNode: SemanticStatusNode
private var progressNode: ASImageNode?
@ -616,6 +706,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
progressNode.image = generateIndefiniteActivityIndicatorImage(color: state.textColor, diameter: diameter, lineWidth: 3.0)
}
var textFrame: CGRect = .zero
if let text = state.text {
let font: UIFont
switch state.font {
@ -627,13 +718,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
self.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: state.textColor)
let textSize = self.textNode.updateLayout(size)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
if self.textNode.frame.width.isZero {
self.textNode.frame = textFrame
} else {
self.textNode.bounds = CGRect(origin: .zero, size: textSize)
transition.updatePosition(node: self.textNode, position: textFrame.center)
}
textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
switch state.background {
case let .color(backgroundColor):
@ -669,6 +754,40 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
}
}
if let badge = state.badge {
let badgeNode: BadgeNode
var badgeTransition = transition
if let current = self.badgeNode {
badgeNode = current
} else {
badgeTransition = .immediate
var textColor: UIColor
switch state.background {
case let .color(backgroundColor):
textColor = backgroundColor
case .premium:
textColor = UIColor(rgb: 0x0077ff)
}
badgeNode = BadgeNode(fillColor: state.textColor, strokeColor: .clear, textColor: textColor)
self.badgeNode = badgeNode
self.addSubnode(badgeNode)
}
badgeNode.text = badge
let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
textFrame.origin.x -= badgeSize.width / 2.0
badgeTransition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - badgeSize.height) * 0.5)), size: badgeSize))
} else if let badgeNode = self.badgeNode {
self.badgeNode = nil
badgeNode.removeFromSupernode()
}
if self.textNode.frame.width.isZero {
self.textNode.frame = textFrame
} else {
self.textNode.bounds = CGRect(origin: .zero, size: textFrame.size)
transition.updatePosition(node: self.textNode, position: textFrame.center)
}
if previousState.progress != state.progress {
if state.progress == .center {
self.transitionToProgress()

View File

@ -2491,6 +2491,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
case let .user(userType):
if case let .user(user) = peer {
match = true
if user.id.isVerificationCodes {
match = false
}
if let isBot = userType.isBot {
if isBot != (user.botInfo != nil) {
match = false

View File

@ -878,14 +878,10 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
if file.isInstantVideo {
isRound = true
}
if file.isSticker || file.isAnimatedSticker {
self.playIcon.isHidden = true
if file.isVideo && !file.isAnimated {
self.playIcon.isHidden = false
} else {
if file.isAnimated {
self.playIcon.isHidden = true
} else {
self.playIcon.isHidden = false
}
self.playIcon.isHidden = true
}
if let mediaDimensions = file.dimensions {
dimensions = mediaDimensions.cgSize
@ -2646,6 +2642,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if contentImageIsDisplayedAsAvatar && (file.isSticker || file.isVideoSticker) {
let fitSize = contentImageSize
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
} else if !file.previewRepresentations.isEmpty, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {

View File

@ -2464,6 +2464,9 @@ public final class ChatListNode: ListView {
case let .user(userType):
if case let .user(user) = peer {
match = true
if user.id.isVerificationCodes {
match = false
}
if let isBot = userType.isBot {
if isBot != (user.botInfo != nil) {
match = false

View File

@ -59,7 +59,7 @@ public final class CounterControllerTitleView: UIView {
let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor
let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.with(size: 13.0, traits: .monospacedNumbers), textColor: secondaryTextColor)
self.accessibilityLabel = self.title.title
self.accessibilityValue = self.title.counter

View File

@ -381,7 +381,11 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
- (void)setValue:(CGFloat)value animated:(BOOL)__unused animated
{
_value = MIN(MAX(_lowerBoundValue, MAX(value, _minimumValue)), _maximumValue);
if (_lowerBoundValue > FLT_EPSILON) {
_value = MIN(MAX(_lowerBoundValue, MAX(value, _minimumValue)), _maximumValue);
} else {
_value = MIN(MAX(value, _minimumValue), _maximumValue);
}
[self setNeedsLayout];
}

View File

@ -297,9 +297,24 @@ final class StickerPackEmojisItemNode: GridItemNode {
self.setNeedsLayout()
}
private var visibleRect: CGRect?
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
var y: CGFloat
if absoluteRect.minY > 0.0 {
y = 0.0
} else {
y = absoluteRect.minY * -1.0
}
var rect = CGRect(origin: CGPoint(x: 0.0, y: y), size: CGSize(width: containerSize.width, height: containerSize.height))
rect.size.height += 96.0
self.visibleRect = rect
self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate)
}
func updateVisibleItems(attemptSynchronousLoads: Bool, transition: ContainedViewLayoutTransition) {
guard let item = self.item, !self.size.width.isZero else {
guard let item = self.item, !self.size.width.isZero, let visibleRect = self.visibleRect else {
return
}
@ -321,17 +336,26 @@ final class StickerPackEmojisItemNode: GridItemNode {
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: item.title != nil ? 61.0 : 0.0), size: CGSize(width: itemLayout.width, height: itemLayout.height))
for index in 0 ..< items.count {
var itemFrame = itemLayout.frame(itemIndex: index)
if !visibleRect.intersects(itemFrame) {
continue
}
let item = items[index]
let itemId = EmojiKeyboardItemLayer.Key(
groupId: 0,
itemId: .animation(.file(item.file.fileId))
)
validIds.insert(itemId)
let itemDimensions = item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
let itemNativeFitSize = itemDimensions.fitted(CGSize(width: nativeItemSize, height: nativeItemSize))
let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize))
validIds.insert(itemId)
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
itemFrame.size = itemVisibleFitSize
var updateItemLayerPlaceholder = false
var itemTransition = transition
let itemLayer: EmojiKeyboardItemLayer
@ -426,12 +450,6 @@ final class StickerPackEmojisItemNode: GridItemNode {
itemLayer.layerTintColor = color.cgColor
}
var itemFrame = itemLayout.frame(itemIndex: index)
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
itemFrame.size = itemVisibleFitSize
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size)
itemTransition.updatePosition(layer: itemLayer, position: itemPosition)

View File

@ -34,6 +34,7 @@ swift_library(
"//submodules/ContextUI",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode",
"//submodules/CounterControllerTitleView",
],
visibility = [
"//visibility:public",

View File

@ -9,6 +9,7 @@ import ProgressNavigationButtonNode
import AccountContext
import SearchUI
import ChatListUI
import CounterControllerTitleView
public final class PeerSelectionControllerImpl: ViewController, PeerSelectionController {
private let context: AccountContext
@ -64,6 +65,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: [ReplyMarkupButtonRequestPeerType]?
let multipleSelectionLimit: Int32?
private let hasCreation: Bool
let immediatelyActivateMultipleSelection: Bool
@ -81,6 +83,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
}
}
private(set) var titleView: CounterControllerTitleView?
private var searchContentNode: NavigationBarSearchContentNode?
var tabContainerNode: ChatListFilterTabContainerNode?
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
@ -107,6 +110,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.requestPeerType = params.requestPeerType
self.hasCreation = params.hasCreation
self.immediatelyActivateMultipleSelection = params.immediatelyActivateMultipleSelection
self.multipleSelectionLimit = params.multipleSelectionLimit
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
@ -133,7 +137,13 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
}
}
self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle
if let maxCount = params.multipleSelectionLimit {
self.titleView = CounterControllerTitleView(theme: self.presentationData.theme)
self.titleView?.title = CounterControllerTitle(title: self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle, counter: "0/\(maxCount)")
self.navigationItem.titleView = self.titleView
} else {
self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle
}
if params.forumPeerId == nil {
self.navigationPresentation = .modal

View File

@ -24,6 +24,7 @@ import SolidRoundedButtonNode
import ContextUI
import TextFormat
import ForwardAccessoryPanelNode
import CounterControllerTitleView
final class PeerSelectionControllerNode: ASDisplayNode {
private let context: AccountContext
@ -108,7 +109,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return (self.presentationData, self.presentationDataPromise.get())
}
init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.controller = controller
self.present = present
@ -215,6 +216,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} else {
self.mainContainerNode = nil
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
if let multipleSelectionLimit = controller.multipleSelectionLimit {
self.chatListNode?.selectionLimit = multipleSelectionLimit
}
}
super.init()
@ -926,7 +930,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} else if let chatListNode = self.chatListNode {
chatListNode.selectionCountChanged = { [weak self] count in
if let self {
if let _ = self.controller?.multipleSelectionLimit {
self.countPanelNode?.buttonTitle = self.presentationData.strings.Premium_Gift_ContactSelection_Proceed
} else {
self.countPanelNode?.buttonTitle = self.presentationData.strings.ShareMenu_Send
}
self.countPanelNode?.count = count
if let titleView = self.controller?.titleView, let maxCount = self.controller?.multipleSelectionLimit {
titleView.title = CounterControllerTitle(title: titleView.title.title, counter: "\(count)/\(maxCount)")
}
if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring))
}
@ -1789,10 +1803,11 @@ private final class PeersCountPanelNode: ASDisplayNode {
private var validLayout: (CGFloat, CGFloat, CGFloat)?
var buttonTitle: String = ""
var count: Int = 0 {
didSet {
if self.count != oldValue && self.count > 0 {
self.button.title = self.strings.ShareMenu_Send
self.button.title = self.buttonTitle
self.button.badge = "\(self.count)"
if let (width, sideInset, bottomInset) = self.validLayout {

View File

@ -4060,10 +4060,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.present(controller, in: .window(.root))
}
if case .user = peerType, maxQuantity > 1 {
if case let .user(requestUser) = peerType, maxQuantity > 1, requestUser.isBot == nil && requestUser.isPremium == nil {
let presentationData = self.presentationData
var reachedLimitImpl: ((Int32) -> Void)?
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .requestedUsersSelection, isPeerEnabled: { peer in
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .requestedUsersSelection(isBot: requestUser.isBot, isPremium: requestUser.isPremium), isPeerEnabled: { peer in
if case let .user(user) = peer, user.botInfo == nil {
return true
} else {
@ -4105,7 +4105,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var createNewGroupImpl: (() -> Void)?
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [peerType], hasContactSelector: false, createNewGroup: {
createNewGroupImpl?()
}, hasCreation: true))
}, multipleSelection: maxQuantity > 1, multipleSelectionLimit: maxQuantity > 1 ? maxQuantity : nil, hasCreation: true, immediatelyActivateMultipleSelection: maxQuantity > 1))
controller.peerSelected = { [weak self, weak controller] peer, _ in
guard let strongSelf = self else {
@ -4126,6 +4126,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
}
controller.multiplePeersSelected = { [weak controller] peers, _, _, _, _, _ in
let peerIds = peers.map { $0.id }
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: peerIds).startStandalone()
controller?.dismiss()
}
createNewGroupImpl = { [weak controller] in
switch peerType {
case .user: