mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-04-24 12:15:10 +08:00
simple readme support in repos
This commit is contained in:
@@ -198,5 +198,22 @@ final class RepositoryClient {
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
func fetchReadme(
|
||||
completion: @escaping (Result<String>) -> ()
|
||||
) {
|
||||
githubClient.request(GithubClient.Request(
|
||||
path: "repos/\(owner)/\(name)/readme",
|
||||
completion: { (response, _) in
|
||||
if let json = response.value as? [String: Any],
|
||||
let content = json["content"] as? String,
|
||||
let data = Data(base64Encoded: content, options: [.ignoreUnknownCharacters]),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
completion(.success(text))
|
||||
} else {
|
||||
completion(.error(response.error))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
120
Classes/Repository/RepositoryDataSource.swift
Normal file
120
Classes/Repository/RepositoryDataSource.swift
Normal file
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// RepositoryDataSource.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ryan Nystrom on 9/10/17.
|
||||
// Copyright © 2017 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import IGListKit
|
||||
|
||||
final class RepositoryDataSource {
|
||||
|
||||
static let readmeTitle = NSLocalizedString("Readme", comment: "")
|
||||
static let issuesTitle = NSLocalizedString("Issues", comment: "")
|
||||
static let pullRequestsTitle = NSLocalizedString("Pull Requests", comment: "")
|
||||
|
||||
// mutated by SegmentedControlSectionController
|
||||
let selection: SegmentedControlModel
|
||||
let loadMore = "loadMore" as ListDiffable
|
||||
|
||||
private var readmeModel: RepositoryReadmeModel? = nil
|
||||
private var issues = [RepositoryIssueSummaryModel]()
|
||||
private var pullRequests = [RepositoryIssueSummaryModel]()
|
||||
|
||||
// public for paging
|
||||
private(set) var issuesNextPage: String?
|
||||
private(set) var pullRequestsNextPage: String?
|
||||
|
||||
init(hasIssuesEnabled: Bool) {
|
||||
var items = [RepositoryDataSource.readmeTitle]
|
||||
if hasIssuesEnabled {
|
||||
items.append(RepositoryDataSource.issuesTitle)
|
||||
}
|
||||
items.append(RepositoryDataSource.pullRequestsTitle)
|
||||
selection = SegmentedControlModel(items: items)
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
enum State {
|
||||
case readme
|
||||
case issues
|
||||
case pullRequests
|
||||
}
|
||||
|
||||
var state: State {
|
||||
switch selection.items[selection.selectedIndex] {
|
||||
case RepositoryDataSource.readmeTitle: return .readme
|
||||
case RepositoryDataSource.issuesTitle: return .issues
|
||||
case RepositoryDataSource.pullRequestsTitle: return .pullRequests
|
||||
default: fatalError("Repository selection in unknown state")
|
||||
}
|
||||
}
|
||||
|
||||
var selectionModels: [ListDiffable] {
|
||||
switch state {
|
||||
case .readme:
|
||||
if let model = readmeModel {
|
||||
return [model]
|
||||
}
|
||||
return []
|
||||
case .issues:
|
||||
var models: [ListDiffable] = issues
|
||||
if issuesNextPage != nil {
|
||||
models.append(loadMore)
|
||||
}
|
||||
return models
|
||||
case .pullRequests:
|
||||
var models: [ListDiffable] = pullRequests
|
||||
if pullRequestsNextPage != nil {
|
||||
models.append(loadMore)
|
||||
}
|
||||
return models
|
||||
}
|
||||
}
|
||||
|
||||
func reset(
|
||||
issues: [RepositoryIssueSummaryModel],
|
||||
issuesNextPage: String?,
|
||||
pullRequests: [RepositoryIssueSummaryModel],
|
||||
pullRequestsNextPage: String?
|
||||
) {
|
||||
self.issues = issues
|
||||
self.issuesNextPage = issuesNextPage
|
||||
self.pullRequests = pullRequests
|
||||
self.pullRequestsNextPage = pullRequestsNextPage
|
||||
}
|
||||
|
||||
func setReadme(
|
||||
_ readme: String,
|
||||
width: CGFloat,
|
||||
completion: @escaping () -> ()
|
||||
) {
|
||||
DispatchQueue.global().async {
|
||||
let models = CreateCommentModels(markdown: readme, width: width)
|
||||
let model = RepositoryReadmeModel(models: models)
|
||||
DispatchQueue.main.async {
|
||||
self.readmeModel = model
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setPullRequests(_ pullRequests: [RepositoryIssueSummaryModel], page: String?) {
|
||||
self.pullRequests = pullRequests
|
||||
self.pullRequestsNextPage = page
|
||||
}
|
||||
|
||||
func appendIssues(issues: [RepositoryIssueSummaryModel], page: String?) {
|
||||
self.issues += issues
|
||||
self.issuesNextPage = page
|
||||
}
|
||||
|
||||
func appendPullRequests(pullRequests: [RepositoryIssueSummaryModel], page: String?) {
|
||||
self.pullRequests += pullRequests
|
||||
self.pullRequestsNextPage = page
|
||||
}
|
||||
|
||||
}
|
||||
31
Classes/Repository/RepositoryReadmeModel.swift
Normal file
31
Classes/Repository/RepositoryReadmeModel.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// RepositoryReadmeModel.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ryan Nystrom on 9/10/17.
|
||||
// Copyright © 2017 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import IGListKit
|
||||
|
||||
final class RepositoryReadmeModel: ListDiffable {
|
||||
|
||||
let models: [ListDiffable]
|
||||
|
||||
init(models: [ListDiffable]) {
|
||||
self.models = models
|
||||
}
|
||||
|
||||
// MARK: ListDiffable
|
||||
|
||||
func diffIdentifier() -> NSObjectProtocol {
|
||||
return "readme" as NSObjectProtocol
|
||||
}
|
||||
|
||||
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
|
||||
// for use w/ binding SC
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
64
Classes/Repository/RepositoryReadmeSectionController.swift
Normal file
64
Classes/Repository/RepositoryReadmeSectionController.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// RepositoryReadmeSectionController.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ryan Nystrom on 9/10/17.
|
||||
// Copyright © 2017 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import IGListKit
|
||||
|
||||
final class RepositoryReadmeSectionController: ListBindingSectionController<RepositoryReadmeModel>,
|
||||
ListBindingSectionControllerDataSource {
|
||||
|
||||
private lazy var webviewCache: WebviewCellHeightCache = {
|
||||
return WebviewCellHeightCache(sectionController: self)
|
||||
}()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
dataSource = self
|
||||
}
|
||||
|
||||
// MARK: ListBindingSectionControllerDataSource
|
||||
|
||||
func sectionController(
|
||||
_ sectionController: ListBindingSectionController<ListDiffable>,
|
||||
viewModelsFor object: Any
|
||||
) -> [ListDiffable] {
|
||||
return self.object?.models ?? []
|
||||
}
|
||||
|
||||
func sectionController(
|
||||
_ sectionController: ListBindingSectionController<ListDiffable>,
|
||||
sizeForViewModel viewModel: Any,
|
||||
at index: Int
|
||||
) -> CGSize {
|
||||
guard let width = collectionContext?.containerSize.width else { fatalError("Missing context") }
|
||||
let height = BodyHeightForComment(viewModel: viewModel, width: width, webviewCache: webviewCache)
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
func sectionController(
|
||||
_ sectionController: ListBindingSectionController<ListDiffable>,
|
||||
cellForViewModel viewModel: Any,
|
||||
at index: Int
|
||||
) -> UICollectionViewCell {
|
||||
guard let context = self.collectionContext else { fatalError("Missing context") }
|
||||
|
||||
let cellClass: AnyClass = CellTypeForComment(viewModel: viewModel)
|
||||
let cell = context.dequeueReusableCell(of: cellClass, for: self, at: index)
|
||||
|
||||
ExtraCommentCellConfigure(
|
||||
cell: cell,
|
||||
imageDelegate: nil,
|
||||
htmlDelegate: webviewCache,
|
||||
htmlNavigationDelegate: nil,
|
||||
attributedDelegate: nil
|
||||
)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,21 +25,16 @@ PrimaryViewController {
|
||||
private let repo: RepositoryDetails
|
||||
private let client: RepositoryClient
|
||||
private lazy var feed: Feed = { Feed(viewController: self, delegate: self) }()
|
||||
private let selection: SegmentedControlModel
|
||||
|
||||
private let noIssuesResultsKey = "noIssuesResultsKey" as ListDiffable
|
||||
private let noPullRequestsResultsKey = "noPullRequestsResultsKey" as ListDiffable
|
||||
private let loadMore = "loadMore" as ListDiffable
|
||||
|
||||
private var issues = [RepositoryIssueSummaryModel]()
|
||||
private var issuesNextPage: String?
|
||||
private var pullRequests = [RepositoryIssueSummaryModel]()
|
||||
private var pullRequestsNextPage: String?
|
||||
private let dataSource: RepositoryDataSource
|
||||
|
||||
init(client: GithubClient, repo: RepositoryDetails) {
|
||||
self.repo = repo
|
||||
self.client = RepositoryClient(githubClient: client, owner: repo.owner, name: repo.name)
|
||||
self.selection = SegmentedControlModel.forRepository(repo.hasIssuesEnabled)
|
||||
self.dataSource = RepositoryDataSource(hasIssuesEnabled: repo.hasIssuesEnabled)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@@ -72,14 +67,26 @@ PrimaryViewController {
|
||||
}
|
||||
|
||||
func reload() {
|
||||
client.fetchReadme { result in
|
||||
switch result {
|
||||
case .error: break
|
||||
case .success(let readme):
|
||||
self.dataSource.setReadme(readme, width: self.view.bounds.width, completion: {
|
||||
self.update(dismissRefresh: true, animated: true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
client.load(containerWidth: view.bounds.width) { result in
|
||||
switch result {
|
||||
case .error: break
|
||||
case .success(let payload):
|
||||
self.issues = payload.issues.models
|
||||
self.issuesNextPage = payload.issues.nextPage
|
||||
self.pullRequests = payload.pullRequests.models
|
||||
self.pullRequestsNextPage = payload.pullRequests.nextPage
|
||||
self.dataSource.reset(
|
||||
issues: payload.issues.models,
|
||||
issuesNextPage: payload.issues.nextPage,
|
||||
pullRequests: payload.pullRequests.models,
|
||||
pullRequestsNextPage: payload.pullRequests.nextPage
|
||||
)
|
||||
self.update(dismissRefresh: true, animated: true)
|
||||
}
|
||||
}
|
||||
@@ -88,23 +95,23 @@ PrimaryViewController {
|
||||
func loadNextPage() {
|
||||
let width = view.bounds.width
|
||||
|
||||
if selection.issuesSelected {
|
||||
client.loadMoreIssues(nextPage: issuesNextPage, containerWidth: width, completion: { result in
|
||||
switch dataSource.state {
|
||||
case .readme: return
|
||||
case .issues:
|
||||
client.loadMoreIssues(nextPage: dataSource.issuesNextPage, containerWidth: width, completion: { result in
|
||||
switch result {
|
||||
case .error: break
|
||||
case .success(let payload):
|
||||
self.issues += payload.models
|
||||
self.issuesNextPage = payload.nextPage
|
||||
self.dataSource.appendIssues(issues: payload.models, page: payload.nextPage)
|
||||
self.update(dismissRefresh: true, animated: false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
client.loadMorePullRequests(nextPage: issuesNextPage, containerWidth: width, completion: { result in
|
||||
case .pullRequests:
|
||||
client.loadMorePullRequests(nextPage: dataSource.pullRequestsNextPage, containerWidth: width, completion: { result in
|
||||
switch result {
|
||||
case .error: break
|
||||
case .success(let payload):
|
||||
self.pullRequests += payload.models
|
||||
self.pullRequestsNextPage = payload.nextPage
|
||||
self.dataSource.appendPullRequests(pullRequests: payload.models, page: payload.nextPage)
|
||||
self.update(dismissRefresh: true, animated: false)
|
||||
}
|
||||
})
|
||||
@@ -125,28 +132,16 @@ PrimaryViewController {
|
||||
|
||||
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
|
||||
var builder = [ListDiffable]()
|
||||
builder.append(dataSource.selection)
|
||||
|
||||
if repo.hasIssuesEnabled {
|
||||
builder.append(selection)
|
||||
}
|
||||
let models = dataSource.selectionModels
|
||||
builder += models
|
||||
|
||||
if selection.issuesSelected {
|
||||
if issues.count > 0 {
|
||||
builder += issues as [ListDiffable]
|
||||
if issuesNextPage != nil {
|
||||
builder.append(loadMore)
|
||||
}
|
||||
} else if feed.status == .idle {
|
||||
builder.append(noIssuesResultsKey)
|
||||
}
|
||||
} else if !selection.issuesSelected {
|
||||
if pullRequests.count > 0 {
|
||||
builder += pullRequests as [ListDiffable]
|
||||
if pullRequestsNextPage != nil {
|
||||
builder.append(loadMore)
|
||||
}
|
||||
} else if feed.status == .idle {
|
||||
builder.append(noPullRequestsResultsKey)
|
||||
if models.count == 0, feed.status == .idle {
|
||||
switch dataSource.state {
|
||||
case .readme: break
|
||||
case .issues: builder.append(noIssuesResultsKey)
|
||||
case .pullRequests: builder.append(noPullRequestsResultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,12 +158,14 @@ PrimaryViewController {
|
||||
return RepositoryEmptyResultsSectionController(topInset: controlHeight, topLayoutGuide: topLayoutGuide, type: .issues)
|
||||
} else if object === noPullRequestsResultsKey {
|
||||
return RepositoryEmptyResultsSectionController(topInset: controlHeight, topLayoutGuide: topLayoutGuide, type: .pullRequests)
|
||||
} else if object === selection {
|
||||
} else if object === dataSource.selection {
|
||||
return SegmentedControlSectionController(delegate: self, height: controlHeight)
|
||||
} else if object === loadMore {
|
||||
} else if object === dataSource.loadMore {
|
||||
return LoadMoreSectionController(delegate: self)
|
||||
} else if object is RepositoryIssueSummaryModel {
|
||||
return RepositorySummarySectionController(client: client.githubClient, repo: repo)
|
||||
} else if object is RepositoryReadmeModel {
|
||||
return RepositoryReadmeSectionController()
|
||||
}
|
||||
|
||||
fatalError("Could not find section controller for object")
|
||||
|
||||
@@ -290,6 +290,9 @@
|
||||
29EDFE7C1F65C580005BCCEB /* SplitViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EDFE7B1F65C580005BCCEB /* SplitViewTests.swift */; };
|
||||
29EDFE7D1F65C583005BCCEB /* SplitViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AC90E41F00A7C8000B80E4 /* SplitViewControllerDelegate.swift */; };
|
||||
29EDFE7E1F65C688005BCCEB /* SplitPlaceholderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C33FDA1F127DBB00EC8D40 /* SplitPlaceholderViewController.swift */; };
|
||||
29EDFE801F6606B6005BCCEB /* RepositoryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EDFE7F1F6606B6005BCCEB /* RepositoryDataSource.swift */; };
|
||||
29EDFE821F661562005BCCEB /* RepositoryReadmeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EDFE811F661562005BCCEB /* RepositoryReadmeModel.swift */; };
|
||||
29EDFE841F661776005BCCEB /* RepositoryReadmeSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EDFE831F661776005BCCEB /* RepositoryReadmeSectionController.swift */; };
|
||||
29EE1C121F37C51D0046A54D /* IssueLabelEditCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EE1C111F37C51D0046A54D /* IssueLabelEditCell.swift */; };
|
||||
29EE1C171F3A2E7A0046A54D /* Labels.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29EE1C161F3A2E7A0046A54D /* Labels.storyboard */; };
|
||||
29EE1C191F3A2EDB0046A54D /* LabelTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EE1C181F3A2EDB0046A54D /* LabelTableCell.swift */; };
|
||||
@@ -611,6 +614,9 @@
|
||||
29DA1E8B1F5F8CC40050C64B /* MarkdownAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownAttribute.swift; sourceTree = "<group>"; };
|
||||
29EB1EEE1F425E5100A200B4 /* ForegroundHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForegroundHandler.swift; sourceTree = "<group>"; };
|
||||
29EDFE7B1F65C580005BCCEB /* SplitViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitViewTests.swift; sourceTree = "<group>"; };
|
||||
29EDFE7F1F6606B6005BCCEB /* RepositoryDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryDataSource.swift; sourceTree = "<group>"; };
|
||||
29EDFE811F661562005BCCEB /* RepositoryReadmeModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryReadmeModel.swift; sourceTree = "<group>"; };
|
||||
29EDFE831F661776005BCCEB /* RepositoryReadmeSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryReadmeSectionController.swift; sourceTree = "<group>"; };
|
||||
29EE1C111F37C51D0046A54D /* IssueLabelEditCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueLabelEditCell.swift; sourceTree = "<group>"; };
|
||||
29EE1C161F3A2E7A0046A54D /* Labels.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Labels.storyboard; sourceTree = "<group>"; };
|
||||
29EE1C181F3A2EDB0046A54D /* LabelTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableCell.swift; sourceTree = "<group>"; };
|
||||
@@ -1418,9 +1424,12 @@
|
||||
children = (
|
||||
292ACE191F5CAF9F00C9A02C /* Empty */,
|
||||
986B873B1F2CEB1500AAB55C /* GQL+RepositoryIssueSummaryType.swift */,
|
||||
986B873D1F2E1CE400AAB55C /* RepositoryClient.swift */,
|
||||
29EDFE7F1F6606B6005BCCEB /* RepositoryDataSource.swift */,
|
||||
292ACE171F5C945B00C9A02C /* RepositoryIssueSummaryModel.swift */,
|
||||
986B87331F2CAE9800AAB55C /* RepositoryIssueSummaryType.swift */,
|
||||
986B873D1F2E1CE400AAB55C /* RepositoryClient.swift */,
|
||||
29EDFE811F661562005BCCEB /* RepositoryReadmeModel.swift */,
|
||||
29EDFE831F661776005BCCEB /* RepositoryReadmeSectionController.swift */,
|
||||
986B87371F2CB29700AAB55C /* RepositorySummaryCell.swift */,
|
||||
986B87351F2CB28C00AAB55C /* RepositorySummarySectionController.swift */,
|
||||
986B872F1F2CA90400AAB55C /* RepositoryViewController.swift */,
|
||||
@@ -1717,6 +1726,7 @@
|
||||
292FCAF81EDFCC510026635E /* CollapsibleCell.swift in Sources */,
|
||||
290744BA1F26863100FD9E48 /* UIScrollView+ScrollToBottom.swift in Sources */,
|
||||
29DA1E8C1F5F8CC40050C64B /* MarkdownAttribute.swift in Sources */,
|
||||
29EDFE841F661776005BCCEB /* RepositoryReadmeSectionController.swift in Sources */,
|
||||
29DA1E7F1F5DF2960050C64B /* LoadMoreCell.swift in Sources */,
|
||||
29DA1E791F5DEE8F0050C64B /* SearchLoadingView.swift in Sources */,
|
||||
29CD71171EF22F4200616E85 /* NSAttributedString+Trim.swift in Sources */,
|
||||
@@ -1746,6 +1756,7 @@
|
||||
292FCB071EDFCC510026635E /* IssueCommentReactionCell.swift in Sources */,
|
||||
29316DCD1ECD31E9007CAE3F /* StatusBar.swift in Sources */,
|
||||
29F7F05C1F2A751B00F6075D /* IssueResult.swift in Sources */,
|
||||
29EDFE801F6606B6005BCCEB /* RepositoryDataSource.swift in Sources */,
|
||||
2958406D1EE8EBF3007723C6 /* IssueCommentPhoto.swift in Sources */,
|
||||
290EF56A1F06A821006A2160 /* Notification+NotificationViewModel.swift in Sources */,
|
||||
986B87301F2CA90400AAB55C /* RepositoryViewController.swift in Sources */,
|
||||
@@ -1901,6 +1912,7 @@
|
||||
2963A9341EE2118E0066509C /* ResponderButton.swift in Sources */,
|
||||
2981A8A41EFE9FC700E25EF1 /* GithubEmoji.swift in Sources */,
|
||||
29C9FDD81EC65FEE00EE3A52 /* User.swift in Sources */,
|
||||
29EDFE821F661562005BCCEB /* RepositoryReadmeModel.swift in Sources */,
|
||||
292FCB0A1EDFCC510026635E /* IssueCommentTextCell.swift in Sources */,
|
||||
299C06DA1F0DD87F00C2828E /* GithubClient+Paging.swift in Sources */,
|
||||
2928C78E1F15DF1B0000D06D /* IssueRenamedString.swift in Sources */,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1748</string>
|
||||
<string>1761</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
||||
Reference in New Issue
Block a user