mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-04-29 04:24:58 +08:00
Use AttributedStringCell for IssueLabeledCell (#1071)
* Use AttributedStringCell for IssueLabeledCell * Code review changes
This commit is contained in:
committed by
Ryan Nystrom
parent
bb2828e9c8
commit
a504a87033
@@ -16,5 +16,6 @@ enum MarkdownAttribute {
|
||||
static let linkShorteningDisabled = NSAttributedStringKey(rawValue: "com.freetime.Markdown.link-shortening-disabled-name")
|
||||
static let issue = NSAttributedStringKey(rawValue: "com.freetime.Markdown.issue")
|
||||
static let details = NSAttributedStringKey(rawValue: "com.freetime.Markdown.details")
|
||||
static let label = NSAttributedStringKey(rawValue: "com.freetime.Markdown.label")
|
||||
static let commit = NSAttributedStringKey(rawValue: "com.freetime.Markdown.commit")
|
||||
}
|
||||
|
||||
@@ -348,7 +348,9 @@ extension GithubClient {
|
||||
title: newLabel.name,
|
||||
color: newLabel.color,
|
||||
date: Date(),
|
||||
type: .added
|
||||
type: .added,
|
||||
repoOwner: owner,
|
||||
repoName: repo
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -360,7 +362,9 @@ extension GithubClient {
|
||||
title: oldLabel.name,
|
||||
color: oldLabel.color,
|
||||
date: Date(),
|
||||
type: .removed
|
||||
type: .removed,
|
||||
repoOwner: owner,
|
||||
repoName: repo
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsIssue: Is
|
||||
title: unlabeled.label.name,
|
||||
color: unlabeled.label.color,
|
||||
date: date,
|
||||
type: .removed
|
||||
type: .removed,
|
||||
repoOwner: owner,
|
||||
repoName: repo
|
||||
)
|
||||
results.append(model)
|
||||
} else if let labeled = node.asLabeledEvent,
|
||||
@@ -102,7 +104,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsIssue: Is
|
||||
title: labeled.label.name,
|
||||
color: labeled.label.color,
|
||||
date: date,
|
||||
type: .added
|
||||
type: .added,
|
||||
repoOwner: owner,
|
||||
repoName: repo
|
||||
)
|
||||
results.append(model)
|
||||
} else if let closed = node.asClosedEvent,
|
||||
|
||||
@@ -9,95 +9,19 @@
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
protocol IssueLabeledCellDelegate: class {
|
||||
func didTapActor(cell: IssueLabeledCell)
|
||||
func didTapLabel(cell: IssueLabeledCell)
|
||||
}
|
||||
|
||||
final class IssueLabeledCell: UICollectionViewCell {
|
||||
|
||||
weak var delegate: IssueLabeledCellDelegate?
|
||||
|
||||
private let descriptionButton = UIButton()
|
||||
private let labelButton = UIButton()
|
||||
private let dateLabel = ShowMoreDetailsLabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
descriptionButton.addTarget(self, action: #selector(IssueLabeledCell.onActor), for: .touchUpInside)
|
||||
contentView.addSubview(descriptionButton)
|
||||
descriptionButton.snp.makeConstraints { make in
|
||||
make.left.equalTo(Styles.Sizes.eventGutter)
|
||||
make.centerY.equalTo(contentView)
|
||||
}
|
||||
|
||||
labelButton.setupAsLabel(icon: false)
|
||||
labelButton.addTarget(self, action: #selector(IssueLabeledCell.onLabel), for: .touchUpInside)
|
||||
contentView.addSubview(labelButton)
|
||||
labelButton.snp.makeConstraints { make in
|
||||
make.left.equalTo(descriptionButton.snp.right).offset(Styles.Sizes.inlineSpacing)
|
||||
make.centerY.equalTo(descriptionButton)
|
||||
}
|
||||
|
||||
dateLabel.font = Styles.Fonts.secondary
|
||||
dateLabel.textColor = Styles.Colors.Gray.medium.color
|
||||
contentView.addSubview(dateLabel)
|
||||
dateLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(labelButton.snp.right).offset(Styles.Sizes.inlineSpacing)
|
||||
make.centerY.equalTo(contentView)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layoutContentViewForSafeAreaInsets()
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
|
||||
@objc func onActor() {
|
||||
delegate?.didTapActor(cell: self)
|
||||
}
|
||||
|
||||
@objc func onLabel() {
|
||||
delegate?.didTapLabel(cell: self)
|
||||
}
|
||||
final class IssueLabeledCell: AttributedStringCell {
|
||||
|
||||
static let insets = UIEdgeInsets(
|
||||
top: Styles.Sizes.inlineSpacing,
|
||||
left: Styles.Sizes.eventGutter,
|
||||
bottom: Styles.Sizes.inlineSpacing,
|
||||
right: Styles.Sizes.eventGutter
|
||||
)
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
func configure(_ model: IssueLabeledModel) {
|
||||
let actorAttributes = [
|
||||
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.dark.color,
|
||||
NSAttributedStringKey.font: Styles.Fonts.secondaryBold
|
||||
]
|
||||
let actor = NSAttributedString(string: model.actor, attributes: actorAttributes)
|
||||
|
||||
let actionString: String
|
||||
switch model.type {
|
||||
case .added: actionString = NSLocalizedString(" added", comment: "")
|
||||
case .removed: actionString = NSLocalizedString(" removed", comment: "")
|
||||
}
|
||||
let actionAttributes = [
|
||||
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.medium.color,
|
||||
NSAttributedStringKey.font: Styles.Fonts.secondary
|
||||
]
|
||||
let action = NSAttributedString(string: actionString, attributes: actionAttributes)
|
||||
let descriptionText = NSMutableAttributedString(attributedString: actor)
|
||||
descriptionText.append(action)
|
||||
descriptionButton.setAttributedTitle(descriptionText, for: .normal)
|
||||
|
||||
let color = UIColor.fromHex(model.color)
|
||||
labelButton.backgroundColor = color
|
||||
|
||||
labelButton.setTitle(model.title, for: .normal)
|
||||
labelButton.setTitleColor(color.textOverlayColor, for: .normal)
|
||||
|
||||
dateLabel.setText(date: model.date)
|
||||
set(attributedText: model.attributedString)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ final class IssueLabeledModel: ListDiffable {
|
||||
let color: String
|
||||
let date: Date
|
||||
let type: EventType
|
||||
let attributedString: NSAttributedStringSizing
|
||||
|
||||
init(
|
||||
id: String,
|
||||
@@ -29,7 +30,9 @@ final class IssueLabeledModel: ListDiffable {
|
||||
title: String,
|
||||
color: String,
|
||||
date: Date,
|
||||
type: EventType
|
||||
type: EventType,
|
||||
repoOwner: String,
|
||||
repoName: String
|
||||
) {
|
||||
self.id = id
|
||||
self.actor = actor
|
||||
@@ -37,6 +40,66 @@ final class IssueLabeledModel: ListDiffable {
|
||||
self.color = color
|
||||
self.date = date
|
||||
self.type = type
|
||||
|
||||
// Actor
|
||||
|
||||
let actorAttributes: [NSAttributedStringKey: Any] = [
|
||||
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.dark.color,
|
||||
NSAttributedStringKey.font: Styles.Fonts.secondaryBold,
|
||||
MarkdownAttribute.username: actor
|
||||
]
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: actor, attributes: actorAttributes)
|
||||
|
||||
// Action
|
||||
|
||||
let actionAttributes = [
|
||||
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.medium.color,
|
||||
NSAttributedStringKey.font: Styles.Fonts.secondary
|
||||
]
|
||||
|
||||
let actionString: String
|
||||
|
||||
switch type {
|
||||
case .added: actionString = NSLocalizedString(" added ", comment: "")
|
||||
case .removed: actionString = NSLocalizedString(" removed ", comment: "")
|
||||
}
|
||||
|
||||
attributedString.append(NSAttributedString(string: actionString, attributes: actionAttributes))
|
||||
|
||||
// Label
|
||||
|
||||
let labelColor = color.color
|
||||
let labelDetails = LabelDetails(owner: repoOwner, repo: repoName, label: title)
|
||||
|
||||
let labelAttributes: [NSAttributedStringKey: Any] = [
|
||||
NSAttributedStringKey.font: Styles.Fonts.secondaryBold,
|
||||
NSAttributedStringKey.backgroundColor: labelColor,
|
||||
NSAttributedStringKey.foregroundColor: labelColor.textOverlayColor ?? .black,
|
||||
MarkdownAttribute.label: labelDetails
|
||||
]
|
||||
|
||||
attributedString.append(NSAttributedString(string: title, attributes: labelAttributes))
|
||||
|
||||
// Date
|
||||
|
||||
let dateAttributes: [NSAttributedStringKey: Any] = [
|
||||
NSAttributedStringKey.font: Styles.Fonts.secondary,
|
||||
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.medium.color,
|
||||
MarkdownAttribute.details: DateDetailsFormatter().string(from: date)
|
||||
]
|
||||
|
||||
attributedString.append(NSAttributedString(string: " \(date.agoString)", attributes: dateAttributes))
|
||||
|
||||
// Set
|
||||
|
||||
self.attributedString = NSAttributedStringSizing(
|
||||
containerWidth: 0,
|
||||
attributedText: attributedString,
|
||||
inset: IssueLabeledCell.insets,
|
||||
backgroundColor: Styles.Colors.Gray.lighter.color
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// MARK: ListDiffable
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import Foundation
|
||||
import IGListKit
|
||||
|
||||
final class IssueLabeledSectionController: ListGenericSectionController<IssueLabeledModel>, IssueLabeledCellDelegate {
|
||||
final class IssueLabeledSectionController: ListGenericSectionController<IssueLabeledModel> {
|
||||
|
||||
private let issueModel: IssueDetailsModel
|
||||
|
||||
|
||||
init(issueModel: IssueDetailsModel) {
|
||||
self.issueModel = issueModel
|
||||
super.init()
|
||||
@@ -20,7 +20,8 @@ final class IssueLabeledSectionController: ListGenericSectionController<IssueLab
|
||||
|
||||
override func sizeForItem(at index: Int) -> CGSize {
|
||||
guard let width = collectionContext?.containerSize.width else { fatalError("Collection context must be set") }
|
||||
return CGSize(width: width, height: Styles.Sizes.labelEventHeight)
|
||||
let insets = IssueLabeledCell.insets
|
||||
return CGSize(width: width, height: insets.top + insets.bottom + (object?.attributedString.textViewSize(width).height ?? 0))
|
||||
}
|
||||
|
||||
override func cellForItem(at index: Int) -> UICollectionViewCell {
|
||||
@@ -28,20 +29,8 @@ final class IssueLabeledSectionController: ListGenericSectionController<IssueLab
|
||||
let object = self.object
|
||||
else { fatalError("Missing collection context, cell incorrect type, or object missing") }
|
||||
cell.configure(object)
|
||||
cell.delegate = self
|
||||
cell.delegate = viewController
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: IssueLabeledCellDelegate
|
||||
|
||||
func didTapActor(cell: IssueLabeledCell) {
|
||||
guard let actor = object?.actor else { return }
|
||||
viewController?.presentProfile(login: actor)
|
||||
}
|
||||
|
||||
func didTapLabel(cell: IssueLabeledCell) {
|
||||
guard let label = object?.title else { return }
|
||||
viewController?.presentLabels(owner: issueModel.owner, repo: issueModel.repo, label: label)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -97,7 +97,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsPullReque
|
||||
title: unlabeled.label.name,
|
||||
color: unlabeled.label.color,
|
||||
date: date,
|
||||
type: .removed
|
||||
type: .removed,
|
||||
repoOwner: owner,
|
||||
repoName: repo
|
||||
)
|
||||
results.append(model)
|
||||
} else if let labeled = node.asLabeledEvent,
|
||||
@@ -108,7 +110,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsPullReque
|
||||
title: labeled.label.name,
|
||||
color: labeled.label.color,
|
||||
date: date,
|
||||
type: .added
|
||||
type: .added,
|
||||
repoOwner: owner,
|
||||
repoName: repo
|
||||
)
|
||||
results.append(model)
|
||||
} else if let closed = node.asClosedEvent,
|
||||
|
||||
23
Classes/Models/LabelDetails.swift
Normal file
23
Classes/Models/LabelDetails.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// LabelDetails.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Sherlock, James on 26/11/2017.
|
||||
// Copyright © 2017 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class LabelDetails {
|
||||
|
||||
let owner: String
|
||||
let repo: String
|
||||
let label: String
|
||||
|
||||
init(owner: String, repo: String, label: String) {
|
||||
self.owner = owner
|
||||
self.repo = repo
|
||||
self.label = label
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,7 +63,7 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
textContainer.maximumNumberOfLines = maximumNumberOfLines
|
||||
textContainer.lineFragmentPadding = lineFragmentPadding
|
||||
|
||||
layoutManager = NSLayoutManager()
|
||||
layoutManager = LabelLayoutManager()
|
||||
layoutManager.allowsNonContiguousLayout = allowsNonContiguousLayout
|
||||
layoutManager.hyphenationFactor = hyphenationFactor
|
||||
layoutManager.showsInvisibleCharacters = showsInvisibleCharacters
|
||||
@@ -197,3 +197,50 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class LabelLayoutManager: NSLayoutManager {
|
||||
|
||||
override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) {
|
||||
|
||||
// Get the attributes for the backgroundColor attribute
|
||||
var range = charRange
|
||||
let attributes = textStorage?.attributes(at: charRange.location, effectiveRange: &range)
|
||||
|
||||
// Ensure that this is one of our labels we're dealing with, ignore basic backgroundColor attributes
|
||||
guard attributes?[MarkdownAttribute.label] != nil else {
|
||||
super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color)
|
||||
return
|
||||
}
|
||||
|
||||
// Define label rectangle and rounded path
|
||||
let cornerRadius: CGFloat = 2
|
||||
let rect = rectArray[0].insetBy(dx: -2, dy: 1)
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
|
||||
|
||||
// Define the CoreGraphics context
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
context?.setAllowsAntialiasing(true)
|
||||
context?.setShouldAntialias(true)
|
||||
context?.setLineWidth(cornerRadius * 2)
|
||||
context?.setLineJoin(.round)
|
||||
|
||||
// Draw the label background
|
||||
context?.addPath(path.cgPath)
|
||||
context?.drawPath(using: .fillStroke)
|
||||
|
||||
// Add a small border around the label
|
||||
let borderWidth = 1 / UIScreen.main.scale
|
||||
let strokeRect = rect.insetBy(dx: -2, dy: -borderWidth)
|
||||
let strokePath = UIBezierPath(roundedRect: strokeRect, cornerRadius: cornerRadius)
|
||||
|
||||
Styles.Colors.Gray.border.color.set()
|
||||
context?.setLineWidth(borderWidth)
|
||||
context?.addPath(strokePath.cgPath)
|
||||
context?.drawPath(using: .stroke)
|
||||
|
||||
// Reset color as per Apple docs
|
||||
color.set()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ extension UIViewController: AttributedStringViewDelegate {
|
||||
guard let url = URL(string: "mailTo:\(email)") else { return }
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
func didTapLabel(view: AttributedStringView, label: LabelDetails) {
|
||||
presentLabels(owner: label.owner, repo: label.repo, label: label.label)
|
||||
}
|
||||
|
||||
func didTapCommit(view: AttributedStringView, commit: CommitDetails) {
|
||||
presentCommit(owner: commit.owner, repo: commit.repo, hash: commit.hash)
|
||||
|
||||
@@ -13,6 +13,7 @@ protocol AttributedStringViewDelegate: class {
|
||||
func didTapUsername(view: AttributedStringView, username: String)
|
||||
func didTapEmail(view: AttributedStringView, email: String)
|
||||
func didTapCommit(view: AttributedStringView, commit: CommitDetails)
|
||||
func didTapLabel(view: AttributedStringView, label: LabelDetails)
|
||||
}
|
||||
|
||||
protocol AttributedStringViewIssueDelegate: class {
|
||||
@@ -87,6 +88,8 @@ final class AttributedStringView: UIView {
|
||||
delegate?.didTapEmail(view: self, email: emailString)
|
||||
} else if let issue = attributes[MarkdownAttribute.issue] as? IssueDetailsModel {
|
||||
issueDelegate?.didTapIssue(view: self, issue: issue)
|
||||
} else if let label = attributes[MarkdownAttribute.label] as? LabelDetails {
|
||||
delegate?.didTapLabel(view: self, label: label)
|
||||
} else if let commit = attributes[MarkdownAttribute.commit] as? CommitDetails {
|
||||
delegate?.didTapCommit(view: self, commit: commit)
|
||||
}
|
||||
|
||||
@@ -340,6 +340,7 @@
|
||||
7BBFEE591F8A8A0400C68E47 /* SearchBarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBFEE561F8A8A0400C68E47 /* SearchBarCell.swift */; };
|
||||
7BBFEE5B1F8A8A0400C68E47 /* SearchBarSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBFEE581F8A8A0400C68E47 /* SearchBarSectionController.swift */; };
|
||||
7BF2239D1F91056C006CC9A2 /* File+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF2239C1F91056C006CC9A2 /* File+Filename.swift */; };
|
||||
98003D8D1FCAD7FC00755C17 /* LabelDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */; };
|
||||
98647DF31F758CCF00A4DE7A /* NewIssueTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98647DF21F758CCF00A4DE7A /* NewIssueTableViewController.swift */; };
|
||||
986B87191F2B875800AAB55C /* GithubClient+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986B87181F2B875800AAB55C /* GithubClient+Search.swift */; };
|
||||
986B871B1F2B87DD00AAB55C /* SearchRepoResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986B871A1F2B87DD00AAB55C /* SearchRepoResult.swift */; };
|
||||
@@ -750,6 +751,7 @@
|
||||
7BBFEE581F8A8A0400C68E47 /* SearchBarSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBarSectionController.swift; sourceTree = "<group>"; };
|
||||
7BF2239C1F91056C006CC9A2 /* File+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Filename.swift"; sourceTree = "<group>"; };
|
||||
94840B8CCA6EA8A3EA37FA91 /* Pods-Freetime.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Freetime.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Freetime/Pods-Freetime.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelDetails.swift; sourceTree = "<group>"; };
|
||||
98647DF21F758CCF00A4DE7A /* NewIssueTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewIssueTableViewController.swift; sourceTree = "<group>"; };
|
||||
986B87181F2B875800AAB55C /* GithubClient+Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GithubClient+Search.swift"; sourceTree = "<group>"; };
|
||||
986B871A1F2B87DD00AAB55C /* SearchRepoResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchRepoResult.swift; sourceTree = "<group>"; };
|
||||
@@ -1321,6 +1323,7 @@
|
||||
291929411F3EA8CD0012067B /* File.swift */,
|
||||
7BF2239C1F91056C006CC9A2 /* File+Filename.swift */,
|
||||
2963A93A1EE25F6F0066509C /* LabelableFields+IssueLabelModel.swift */,
|
||||
98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */,
|
||||
291929591F3FB1D20012067B /* Link.swift */,
|
||||
29C9FDBA1EC65FEE00EE3A52 /* NotificationResponse.swift */,
|
||||
29C9FDBB1EC65FEE00EE3A52 /* Organization.swift */,
|
||||
@@ -2176,6 +2179,7 @@
|
||||
292FCAF81EDFCC510026635E /* CollapsibleCell.swift in Sources */,
|
||||
295F52AD1EF1BE83000B53CF /* CommentModelsFromMarkdown.swift in Sources */,
|
||||
2919295A1F3FB1D20012067B /* Content.swift in Sources */,
|
||||
98003D8D1FCAD7FC00755C17 /* LabelDetails.swift in Sources */,
|
||||
297DD5E11F061BBE006E7E63 /* CreateProfileViewController.swift in Sources */,
|
||||
29A195041EC74C4800C3E289 /* Date+Display.swift in Sources */,
|
||||
29AAB7171FB4A2AE001D5E6A /* BoundedImageSize.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user