Chat moderation improvements

This commit is contained in:
Isaac 2024-04-22 20:50:06 +04:00
parent c85315e756
commit 59f4ec2cdd
6 changed files with 305 additions and 73 deletions

View File

@ -165,6 +165,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
public let peer: ContactsPeerItemPeer
let status: ContactsPeerItemStatus
let badge: ContactsPeerItemBadge?
let rightLabelText: String?
let requiresPremiumForMessaging: Bool
let enabled: Bool
let selection: ContactsPeerItemSelection
@ -202,6 +203,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
peer: ContactsPeerItemPeer,
status: ContactsPeerItemStatus,
badge: ContactsPeerItemBadge? = nil,
rightLabelText: String? = nil,
requiresPremiumForMessaging: Bool = false,
enabled: Bool,
selection: ContactsPeerItemSelection,
@ -233,6 +235,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.peer = peer
self.status = status
self.badge = badge
self.rightLabelText = rightLabelText
self.requiresPremiumForMessaging = requiresPremiumForMessaging
self.enabled = enabled
self.selection = selection
@ -421,6 +424,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var actionButtonNodes: [HighlightableButtonNode]?
private var moreButtonNode: MoreButtonNode?
private var arrowButtonNode: HighlightableButtonNode?
private var rightLabelTextNode: TextNode?
private var avatarTapRecognizer: UITapGestureRecognizer?
@ -579,6 +583,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
if let rightLabelTextNode = strongSelf.rightLabelTextNode {
transition.updateTransform(node: rightLabelTextNode, transform: CGAffineTransformMakeTranslation(isExtracted ? -24.0 : 0.0, 0.0))
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
@ -588,6 +596,20 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
override public func didLoad() {
super.didLoad()
self.updateEnableGestures()
}
private func updateEnableGestures() {
if let item = self.layoutParams?.0, !item.options.isEmpty {
self.view.disablesInteractiveTransitionGestureRecognizer = false
} else {
self.view.disablesInteractiveTransitionGestureRecognizer = false
}
}
public override func secondaryAction(at point: CGPoint) {
guard let item = self.item, let contextAction = item.contextAction else {
return
@ -665,6 +687,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let currentSelectionNode = self.selectionNode
let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
let makeRightLabelTextLayout = TextNode.asyncLayout(self.rightLabelTextNode)
let currentItem = self.layoutParams?.0
@ -712,6 +735,13 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
var rightLabelTextLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let rightLabelText = item.rightLabelText {
let rightLabelTextLayoutAndApplyValue = makeRightLabelTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: rightLabelText, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 20.0, height: 100.0)))
rightLabelTextLayoutAndApply = rightLabelTextLayoutAndApplyValue
rightInset -= 6.0 + rightLabelTextLayoutAndApplyValue.0.size.width
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
var credibilityIcon: EmojiStatusComponent.Content?
@ -1117,7 +1147,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
transition = .immediate
}
let revealOffset = strongSelf.revealOffset
let revealOffset: CGFloat = 0.0
if let _ = updatedTheme {
switch item.style {
@ -1479,6 +1509,28 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
if let (rightLabelTextLayout, rightLabelTextApply) = rightLabelTextLayoutAndApply {
let rightLabelTextNode = rightLabelTextApply()
var rightLabelTextTransition = transition
if rightLabelTextNode !== strongSelf.rightLabelTextNode {
strongSelf.rightLabelTextNode?.removeFromSupernode()
strongSelf.rightLabelTextNode = rightLabelTextNode
strongSelf.offsetContainerNode.addSubnode(rightLabelTextNode)
rightLabelTextTransition = .immediate
}
var rightLabelTextFrame = CGRect(x: revealOffset + params.width - params.rightInset - 8.0 - rightLabelTextLayout.size.width, y: floor((nodeLayout.contentSize.height - rightLabelTextLayout.size.height) / 2.0), width: rightLabelTextLayout.size.width, height: rightLabelTextLayout.size.height)
if let arrowButtonImage = arrowButtonImage {
rightLabelTextFrame.origin.x -= arrowButtonImage.size.width + 6.0
}
rightLabelTextNode.bounds = CGRect(origin: CGPoint(), size: rightLabelTextFrame.size)
rightLabelTextTransition.updatePosition(node: rightLabelTextNode, position: rightLabelTextFrame.center)
} else if let rightLabelTextNode = strongSelf.rightLabelTextNode {
strongSelf.rightLabelTextNode = nil
rightLabelTextNode.removeFromSupernode()
}
if let updatedSelectionNode = updatedSelectionNode {
let hadSelectionNode = strongSelf.selectionNode != nil
if strongSelf.selectionNode !== updatedSelectionNode {
@ -1533,6 +1585,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
}
strongSelf.updateEnableGestures()
}
})
} else {
@ -1553,57 +1607,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let item = self.item, let params = self.layoutParams?.1 {
var leftInset: CGFloat = 65.0 + params.leftInset
switch item.selection {
case .none:
break
case .selectable:
leftInset += 28.0
}
var avatarFrame = self.avatarNode.frame
avatarFrame.origin.x = offset + leftInset - 50.0
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
var titleFrame = self.titleNode.frame
titleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.titleNode, frame: titleFrame)
var statusFrame = self.statusNode.frame
let previousStatusFrame = statusFrame
statusFrame.origin.x = leftInset + offset
if let statusIconImage = self.statusIconNode?.image {
statusFrame.origin.x += statusIconImage.size.width + 1.0
}
self.statusNode.frame = statusFrame
transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
var nextIconX = titleFrame.maxX
if let credibilityIconView = self.credibilityIconView {
var iconFrame = credibilityIconView.frame
iconFrame.origin.x = nextIconX + 4.0
nextIconX += 4.0 + iconFrame.width
transition.updateFrame(view: credibilityIconView, frame: iconFrame)
}
if let verifiedIconView = self.verifiedIconView {
var iconFrame = verifiedIconView.frame
iconFrame.origin.x = nextIconX + 4.0
nextIconX += 4.0 + iconFrame.width
transition.updateFrame(view: verifiedIconView, frame: iconFrame)
}
if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode {
var badgeBackgroundFrame = badgeBackgroundNode.frame
badgeBackgroundFrame.origin.x = offset + params.width - params.rightInset - badgeBackgroundFrame.width - 6.0
var badgeTextFrame = badgeTextNode.frame
badgeTextFrame.origin.x = badgeBackgroundFrame.midX - badgeTextFrame.width / 2.0
transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame)
transition.updateFrame(node: badgeTextNode, frame: badgeTextFrame)
}
}
var offsetContainerBounds = self.offsetContainerNode.bounds
offsetContainerBounds.origin.x = -offset
transition.updateBounds(node: self.offsetContainerNode, bounds: offsetContainerBounds)
}
override public func revealOptionsInteractivelyOpened() {

View File

@ -616,6 +616,10 @@ private final class AdminUserActionsSheetComponent: Component {
canBanEveryone = false
continue
}
if let banInfo = peer.participant.banInfo, !banInfo.isMember {
canBanEveryone = false
continue
}
switch peer.participant {
case .creator:

View File

@ -144,6 +144,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen",
"//submodules/TelegramUI/Components/Settings/PeerSelectionScreen",
"//submodules/ConfettiEffect",
"//submodules/ContactsPeerItem",
],
visibility = [
"//visibility:public",

View File

@ -14,6 +14,7 @@ import MergeLists
import ItemListUI
import PeerInfoVisualMediaPaneNode
import PeerInfoPaneNode
import ContactsPeerItem
private struct PeerMembersListTransaction {
let deletions: [ListViewDeleteItem]
@ -84,7 +85,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
}
}
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> ListViewItem {
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem {
switch self {
case let .addMember(_, text):
return ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: text, alwaysPlain: true, sectionId: 0, height: .compactPeerList, color: .accent, editing: false, action: {
@ -124,23 +125,103 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
}))
}
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: EnginePeer(member.peer), presence: member.presence.flatMap(EnginePeer.Presence.init), text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: member.id != context.account.peerId, sectionId: 0, action: {
action(member, .open)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true, storyStats: member.storyStats, openStories: { sourceView in
action(member, .openStories(sourceView: sourceView))
})
let presence: EnginePeer.Presence
if member.peer.id == context.account.peerId {
presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0)
} else if let value = member.presence {
presence = EnginePeer.Presence(value)
} else {
presence = EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0)
}
return ContactsPeerItem(
presentationData: ItemListPresentationData(presentationData),
style: .plain,
sectionId: 0,
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .peer,
peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)),
status: .presence(presence, presentationData.dateTimeFormat),
rightLabelText: label,
enabled: true,
selection: .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
options: options,
additionalActions: [],
actionIcon: .none,
index: nil,
header: nil,
action: { _ in
action(member, .open)
},
disabledAction: nil,
setPeerIdWithRevealedOptions: { _, _ in
},
deletePeer: nil,
itemHighlighting: nil,
contextAction: contextAction == nil ? nil : { node, gesture, _ in
contextAction?(member, node, gesture)
},
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
storyStats: member.storyStats.flatMap { storyStats in
return (
total: storyStats.totalCount,
unseen: storyStats.unseenCount,
hasUnseenCloseFriends: storyStats.hasUnseenCloseFriends
)
},
openStories: { _, sourceNode in
action(member, .openStories(sourceView: sourceNode.view))
}
)
/*return ItemListPeerItem(
presentationData: ItemListPresentationData(presentationData),
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
context: context,
peer: EnginePeer(member.peer),
presence: member.presence.flatMap(EnginePeer.Presence.init),
text: .presence,
label: label == nil ? .none : .text(label!, .standard),
editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false),
revealOptions: ItemListPeerItemRevealOptions(options: options),
switchValue: nil,
enabled: true,
selectable: member.id != context.account.peerId,
sectionId: 0,
action: {
action(member, .open)
},
setPeerIdWithRevealedOptions: { _, _ in
},
removePeer: { _ in
},
contextAction: contextAction == nil ? nil : { node, gesture in
contextAction?(member, node, gesture)
},
hasTopStripe: false,
noInsets: true,
noCorners: true,
disableInteractiveTransitionIfNecessary: true,
storyStats: member.storyStats,
openStories: { sourceView in
action(member, .openStories(sourceView: sourceView))
}
)*/
}
}
}
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> PeerMembersListTransaction {
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction {
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.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action, contextAction: contextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action, contextAction: contextAction), directionHint: nil) }
return PeerMembersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count)
}
@ -276,7 +357,79 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: { [weak self] in
self?.addMemberAction()
}, action: { [weak self] member, action in
self?.action(member, action)
guard let self else {
return
}
if case .open = action {
self.listNode.clearHighlightAnimated(true)
}
self.action(member, action)
}, contextAction: { [weak self] member, sourceNode, gesture in
guard let self else {
return
}
var node: ContextExtractedContentContainingNode?
if let sourceNode = sourceNode as? ContextExtractedContentContainingNode {
node = sourceNode
} else {
for subnode in sourceNode.subnodes ?? [] {
if let subnode = subnode as? ContextExtractedContentContainingNode {
node = subnode
break
}
}
}
guard let node else {
gesture?.cancel()
return
}
let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer, member: member)
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
let action = self.action
if actions.contains(.promote) && enclosingPeer is TelegramChannel {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_ActionPromote, icon: { _ in
return nil
}, action: { c, _ in
c.dismiss(completion: {
action(member, .promote)
})
})))
}
if actions.contains(.restrict) {
if enclosingPeer is TelegramChannel {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_ActionRestrict, icon: { _ in
return nil
}, action: { c, _ in
c.dismiss(completion: {
action(member, .restrict)
})
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { _ in
return nil
}, action: { c, _ in
c.dismiss(completion: {
action(member, .remove)
})
})))
}
if items.isEmpty {
gesture?.cancel()
return
}
let dismissPromise = ValuePromise<Bool>(false)
let source = PeerInfoMemberExtractedContentSource(sourceNode: node, keepInPlace: false, blurBackground: true, centerVertically: false, shouldBeDismissed: dismissPromise.get())
let contextController = ContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.parentController?.presentInGlobalOverlay(contextController)
})
self.enclosingPeer = enclosingPeer
self.currentEntries = entries
@ -335,3 +488,30 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
func updateSelectedMessages(animated: Bool) {
}
}
final class PeerInfoMemberExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = false
let blurBackground: Bool
private let sourceNode: ContextExtractedContentContainingNode
var centerVertically: Bool
var shouldBeDismissed: Signal<Bool, NoError>
init(sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal<Bool, NoError>) {
self.sourceNode = sourceNode
self.keepInPlace = keepInPlace
self.blurBackground = blurBackground
self.centerVertically = centerVertically
self.shouldBeDismissed = shouldBeDismissed
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -648,6 +648,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
}
return [.leftCenter, .rightCenter]
}
if case .members = currentPaneKey {
if index == 0 {
return .leftCenter
}
return [.leftCenter, .rightCenter]
}
if strongSelf.currentPane?.node.navigationContentNode != nil {
return []
}

View File

@ -28,6 +28,9 @@ extension ChatControllerImpl {
//TODO:localize
var title: String? = messageIds.count == 1 ? "Message Deleted" : "Messages Deleted"
if !result.deleteAllFromPeers.isEmpty {
title = "Messages Deleted"
}
var text: String = ""
var undoRights: [EnginePeer.Id: InitialBannedRights] = [:]
@ -95,6 +98,9 @@ extension ChatControllerImpl {
if text.isEmpty {
text = messageIds.count == 1 ? "Message Deleted." : "Messages Deleted."
if !result.deleteAllFromPeers.isEmpty {
text = "Messages Deleted."
}
title = nil
}
@ -134,6 +140,9 @@ extension ChatControllerImpl {
var signal = combineLatest(authors.map { author in
self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id)
|> map { result -> (Peer, ChannelParticipant?) in
return (author, result)
}
})
let disposables = MetaDisposable()
self.navigationActionDisposable.set(disposables)
@ -166,7 +175,7 @@ extension ChatControllerImpl {
}
disposables.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] participants in
|> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants in
guard let self else {
return
}
@ -179,13 +188,23 @@ extension ChatControllerImpl {
}
var renderedParticipants: [RenderedChannelParticipant] = []
var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:]
for maybeParticipant in participants {
guard let participant = maybeParticipant else {
continue
}
guard let peer = authors.first(where: { $0.id == participant.peerId }) else {
continue
for (author, maybeParticipant) in authorsAndParticipants {
let participant: ChannelParticipant
if let maybeParticipant {
participant = maybeParticipant
} else {
participant = .member(id: author.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo(
rights: TelegramChatBannedRights(
flags: [.banReadMessages],
untilDate: Int32.max
),
restrictedBy: self.context.account.peerId,
timestamp: 0,
isMember: false
), rank: nil)
}
let peer = author
renderedParticipants.append(RenderedChannelParticipant(
participant: participant,
peer: peer
@ -254,10 +273,26 @@ extension ChatControllerImpl {
}
disposables.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] participant in
guard let self, let participant else {
|> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant in
guard let self else {
return
}
let participant: ChannelParticipant
if let maybeParticipant {
participant = maybeParticipant
} else {
participant = .member(id: author.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo(
rights: TelegramChatBannedRights(
flags: [.banReadMessages],
untilDate: Int32.max
),
restrictedBy: self.context.account.peerId,
timestamp: 0,
isMember: false
), rank: nil)
}
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: author.id)