Remove Mark Read on Scroll code
This commit is contained in:
parent
bb120b5e3c
commit
09652bff81
@ -33,7 +33,6 @@ final class AppDefaults {
|
|||||||
static let detailFontSize = "detailFontSize"
|
static let detailFontSize = "detailFontSize"
|
||||||
static let openInBrowserInBackground = "openInBrowserInBackground"
|
static let openInBrowserInBackground = "openInBrowserInBackground"
|
||||||
static let subscribeToFeedsInDefaultBrowser = "subscribeToFeedsInDefaultBrowser"
|
static let subscribeToFeedsInDefaultBrowser = "subscribeToFeedsInDefaultBrowser"
|
||||||
static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
|
|
||||||
static let articleTextSize = "articleTextSize"
|
static let articleTextSize = "articleTextSize"
|
||||||
static let refreshInterval = "refreshInterval"
|
static let refreshInterval = "refreshInterval"
|
||||||
static let addWebFeedAccountID = "addWebFeedAccountID"
|
static let addWebFeedAccountID = "addWebFeedAccountID"
|
||||||
@ -290,16 +289,6 @@ final class AppDefaults {
|
|||||||
return AppDefaults.bool(for: Key.timelineShowsSeparators)
|
return AppDefaults.bool(for: Key.timelineShowsSeparators)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var markArticlesAsReadOnScroll: Bool {
|
|
||||||
get {
|
|
||||||
return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll)
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var articleTextSize: ArticleTextSize {
|
var articleTextSize: ArticleTextSize {
|
||||||
get {
|
get {
|
||||||
let rawValue = UserDefaults.standard.integer(forKey: Key.articleTextSize)
|
let rawValue = UserDefaults.standard.integer(forKey: Key.articleTextSize)
|
||||||
|
@ -111,9 +111,6 @@ private extension TimelineViewController {
|
|||||||
|
|
||||||
func markArticles(_ articles: [Article], read: Bool) {
|
func markArticles(_ articles: [Article], read: Bool) {
|
||||||
markArticles(articles, statusKey: .read, flag: read)
|
markArticles(articles, statusKey: .read, flag: read)
|
||||||
for article in articles {
|
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func markArticles(_ articles: [Article], starred: Bool) {
|
func markArticles(_ articles: [Article], starred: Bool) {
|
||||||
|
@ -70,7 +70,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
if showsSearchResults {
|
if showsSearchResults {
|
||||||
fetchAndReplaceArticlesAsync()
|
fetchAndReplaceArticlesAsync()
|
||||||
} else {
|
} else {
|
||||||
resetMarkAsReadOnScroll()
|
|
||||||
fetchAndReplaceArticlesSync()
|
fetchAndReplaceArticlesSync()
|
||||||
if articles.count > 0 {
|
if articles.count > 0 {
|
||||||
tableView.scrollRowToVisible(0)
|
tableView.scrollRowToVisible(0)
|
||||||
@ -140,10 +139,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
|
|
||||||
var undoableCommands = [UndoableCommand]()
|
var undoableCommands = [UndoableCommand]()
|
||||||
|
|
||||||
var articlesWithManuallyChangedReadStatus: Set<Article> = Set()
|
|
||||||
|
|
||||||
private var isScrolling = false
|
|
||||||
|
|
||||||
private var fetchSerialNumber = 0
|
private var fetchSerialNumber = 0
|
||||||
private let fetchRequestQueue = FetchRequestQueue()
|
private let fetchRequestQueue = FetchRequestQueue()
|
||||||
private var exceptionArticleFetcher: ArticleFetcher?
|
private var exceptionArticleFetcher: ArticleFetcher?
|
||||||
@ -197,10 +192,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
private let keyboardDelegate = TimelineKeyboardDelegate()
|
private let keyboardDelegate = TimelineKeyboardDelegate()
|
||||||
private var timelineShowsSeparatorsObserver: NSKeyValueObservation?
|
private var timelineShowsSeparatorsObserver: NSKeyValueObservation?
|
||||||
|
|
||||||
private let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0)
|
|
||||||
|
|
||||||
private var markBottomArticlesAsReadWorkItem: DispatchWorkItem?
|
|
||||||
|
|
||||||
convenience init(delegate: TimelineDelegate) {
|
convenience init(delegate: TimelineDelegate) {
|
||||||
self.init(nibName: "TimelineTableView", bundle: nil)
|
self.init(nibName: "TimelineTableView", bundle: nil)
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
@ -232,17 +223,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||||
|
|
||||||
if let scrollView = self.tableView.enclosingScrollView {
|
|
||||||
scrollView.contentView.postsBoundsChangedNotifications = true
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll(notification:)), name: NSView.boundsDidChangeNotification, object: scrollView.contentView)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewWillStartLiveScroll(notification:)), name: NSScrollView.willStartLiveScrollNotification, object: scrollView)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidEndLiveScroll(notification:)), name: NSScrollView.didEndLiveScrollNotification, object: scrollView)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
didRegisterForNotifications = true
|
didRegisterForNotifications = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,8 +281,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func restoreState(from state: [AnyHashable : Any]) {
|
func restoreState(from state: [AnyHashable : Any]) {
|
||||||
resetMarkAsReadOnScroll()
|
|
||||||
|
|
||||||
guard let readArticlesFilterStateKeys = state[UserInfoKey.readArticlesFilterStateKeys] as? [[AnyHashable: AnyHashable]],
|
guard let readArticlesFilterStateKeys = state[UserInfoKey.readArticlesFilterStateKeys] as? [[AnyHashable: AnyHashable]],
|
||||||
let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool] else {
|
let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool] else {
|
||||||
return
|
return
|
||||||
@ -346,66 +324,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func scrollViewDidScroll(notification: Notification){
|
|
||||||
if isScrolling {
|
|
||||||
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func scrollViewWillStartLiveScroll(notification: Notification){
|
|
||||||
isScrolling = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func scrollViewDidEndLiveScroll(notification: Notification){
|
|
||||||
isScrolling = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func scrollPositionDidChange(){
|
|
||||||
if !AppDefaults.shared.markArticlesAsReadOnScroll {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark all articles as read when the bottom of the feed is reached
|
|
||||||
let lastRowIndex = articles.count - 1
|
|
||||||
let atBottom = tableView.rows(in: tableView.visibleRect).contains(lastRowIndex)
|
|
||||||
|
|
||||||
if atBottom && markBottomArticlesAsReadWorkItem == nil {
|
|
||||||
let task = DispatchWorkItem {
|
|
||||||
let articlesToMarkAsRead = self.articles.filter { !$0.status.read && !self.articlesWithManuallyChangedReadStatus.contains($0) }
|
|
||||||
|
|
||||||
if articlesToMarkAsRead.isEmpty { return }
|
|
||||||
guard let undoManager = self.undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMarkAsRead, markingRead: true, undoManager: undoManager) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.runCommand(markReadCommand)
|
|
||||||
self.markBottomArticlesAsReadWorkItem = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
markBottomArticlesAsReadWorkItem = task
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task)
|
|
||||||
} else if !atBottom, let task = markBottomArticlesAsReadWorkItem {
|
|
||||||
task.cancel()
|
|
||||||
markBottomArticlesAsReadWorkItem = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Mark articles scrolled out of sight at the top as read
|
|
||||||
let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location
|
|
||||||
let unreadArticlesScrolledAway = articles.articlesAbove(position: firstVisibleRowIndex).filter { !$0.status.read && !articlesWithManuallyChangedReadStatus.contains($0) }
|
|
||||||
|
|
||||||
if unreadArticlesScrolledAway.isEmpty { return }
|
|
||||||
|
|
||||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: unreadArticlesScrolledAway, markingRead: true, undoManager: undoManager) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runCommand(markReadCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetMarkAsReadOnScroll() {
|
|
||||||
articlesWithManuallyChangedReadStatus.removeAll()
|
|
||||||
markBottomArticlesAsReadWorkItem?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
|
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
|
||||||
guard !selectedArticles.isEmpty else {
|
guard !selectedArticles.isEmpty else {
|
||||||
return
|
return
|
||||||
@ -427,9 +345,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
runCommand(markReadCommand)
|
runCommand(markReadCommand)
|
||||||
for article in selectedArticles {
|
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
|
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
|
||||||
@ -437,9 +352,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
runCommand(markUnreadCommand)
|
runCommand(markUnreadCommand)
|
||||||
for article in selectedArticles {
|
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func copy(_ sender: Any?) {
|
@IBAction func copy(_ sender: Any?) {
|
||||||
@ -991,7 +903,6 @@ extension TimelineViewController: NSTableViewDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.runCommand(markUnreadCommand)
|
self.runCommand(markUnreadCommand)
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleArticleStarred(_ article: Article) {
|
private func toggleArticleStarred(_ article: Article) {
|
||||||
|
@ -46,7 +46,6 @@ final class AppDefaults {
|
|||||||
static let firstRunDate = "firstRunDate"
|
static let firstRunDate = "firstRunDate"
|
||||||
static let timelineGroupByFeed = "timelineGroupByFeed"
|
static let timelineGroupByFeed = "timelineGroupByFeed"
|
||||||
static let refreshClearsReadArticles = "refreshClearsReadArticles"
|
static let refreshClearsReadArticles = "refreshClearsReadArticles"
|
||||||
static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
|
|
||||||
static let timelineNumberOfLines = "timelineNumberOfLines"
|
static let timelineNumberOfLines = "timelineNumberOfLines"
|
||||||
static let timelineIconDimension = "timelineIconSize"
|
static let timelineIconDimension = "timelineIconSize"
|
||||||
static let timelineSortDirection = "timelineSortDirection"
|
static let timelineSortDirection = "timelineSortDirection"
|
||||||
@ -160,15 +159,6 @@ final class AppDefaults {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var markArticlesAsReadOnScroll: Bool {
|
|
||||||
get {
|
|
||||||
return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll)
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var timelineSortDirection: ComparisonResult {
|
var timelineSortDirection: ComparisonResult {
|
||||||
get {
|
get {
|
||||||
return AppDefaults.sortDirection(for: Key.timelineSortDirection)
|
return AppDefaults.sortDirection(for: Key.timelineSortDirection)
|
||||||
@ -246,7 +236,6 @@ final class AppDefaults {
|
|||||||
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
|
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
|
||||||
Key.timelineGroupByFeed: false,
|
Key.timelineGroupByFeed: false,
|
||||||
Key.refreshClearsReadArticles: false,
|
Key.refreshClearsReadArticles: false,
|
||||||
Key.markArticlesAsReadOnScroll: false,
|
|
||||||
Key.timelineNumberOfLines: 2,
|
Key.timelineNumberOfLines: 2,
|
||||||
Key.timelineIconDimension: IconSize.medium.rawValue,
|
Key.timelineIconDimension: IconSize.medium.rawValue,
|
||||||
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
|
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
|
||||||
|
@ -418,10 +418,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if scrollView.isTracking {
|
|
||||||
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
|
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
|
||||||
@ -518,54 +516,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
|
|
||||||
@objc func scrollPositionDidChange() {
|
@objc func scrollPositionDidChange() {
|
||||||
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
|
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
|
||||||
|
|
||||||
if !AppDefaults.shared.markArticlesAsReadOnScroll {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark all articles as read when the bottom of the feed is reached
|
|
||||||
if let lastVisibleRowIndexPath = tableView.indexPathsForVisibleRows?.last {
|
|
||||||
let atBottom = dataSource.itemIdentifier(for: lastVisibleRowIndexPath) == coordinator.articles.last
|
|
||||||
|
|
||||||
if atBottom && coordinator.markBottomArticlesAsReadWorkItem == nil {
|
|
||||||
let task = DispatchWorkItem {
|
|
||||||
let articlesToMarkAsRead = self.coordinator.articles.filter { !$0.status.read && !self.coordinator.articlesWithManuallyChangedReadStatus.contains($0) }
|
|
||||||
|
|
||||||
if articlesToMarkAsRead.isEmpty { return }
|
|
||||||
self.coordinator.markAllAsRead(articlesToMarkAsRead)
|
|
||||||
self.coordinator.markBottomArticlesAsReadWorkItem = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
coordinator.markBottomArticlesAsReadWorkItem = task
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task)
|
|
||||||
} else if !atBottom, let task = coordinator.markBottomArticlesAsReadWorkItem {
|
|
||||||
task.cancel()
|
|
||||||
coordinator.markBottomArticlesAsReadWorkItem = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark articles scrolled out of sight at the top as read
|
|
||||||
guard let visibleRowIndexPaths = tableView.indexPathsForVisibleRows, visibleRowIndexPaths.count > 0 else { return }
|
|
||||||
let firstVisibleRowIndexPath = visibleRowIndexPaths[0]
|
|
||||||
|
|
||||||
guard let firstVisibleArticle = dataSource.itemIdentifier(for: firstVisibleRowIndexPath) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let unreadArticlesScrolledAway = coordinator.articles
|
|
||||||
.articlesAbove(article: firstVisibleArticle)
|
|
||||||
.filter({ !coordinator.articlesWithManuallyChangedReadStatus.contains($0) })
|
|
||||||
.unreadArticles() else { return }
|
|
||||||
|
|
||||||
coordinator.markAllAsRead(unreadArticlesScrolledAway)
|
|
||||||
|
|
||||||
for article in unreadArticlesScrolledAway {
|
|
||||||
if let indexPath = dataSource.indexPath(for: article) {
|
|
||||||
if let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell {
|
|
||||||
configure(cell, article: article)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Reloading
|
// MARK: Reloading
|
||||||
|
@ -182,9 +182,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
private(set) var showFeedNames = ShowFeedName.none
|
private(set) var showFeedNames = ShowFeedName.none
|
||||||
private(set) var showIcons = false
|
private(set) var showIcons = false
|
||||||
|
|
||||||
var articlesWithManuallyChangedReadStatus: Set<Article> = Set()
|
|
||||||
var markBottomArticlesAsReadWorkItem: DispatchWorkItem?
|
|
||||||
|
|
||||||
var prevFeedIndexPath: IndexPath? {
|
var prevFeedIndexPath: IndexPath? {
|
||||||
guard let indexPath = currentFeedIndexPath else {
|
guard let indexPath = currentFeedIndexPath else {
|
||||||
return nil
|
return nil
|
||||||
@ -786,9 +783,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
articlesWithManuallyChangedReadStatus.removeAll()
|
|
||||||
markBottomArticlesAsReadWorkItem?.cancel()
|
|
||||||
|
|
||||||
currentFeedIndexPath = indexPath
|
currentFeedIndexPath = indexPath
|
||||||
masterFeedViewController.updateFeedSelection(animations: animations)
|
masterFeedViewController.updateFeedSelection(animations: animations)
|
||||||
|
|
||||||
@ -1079,28 +1073,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
func markAsReadForCurrentArticle() {
|
func markAsReadForCurrentArticle() {
|
||||||
if let article = currentArticle {
|
if let article = currentArticle {
|
||||||
markArticlesWithUndo([article], statusKey: .read, flag: true)
|
markArticlesWithUndo([article], statusKey: .read, flag: true)
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAsUnreadForCurrentArticle() {
|
func markAsUnreadForCurrentArticle() {
|
||||||
if let article = currentArticle {
|
if let article = currentArticle {
|
||||||
markArticlesWithUndo([article], statusKey: .read, flag: false)
|
markArticlesWithUndo([article], statusKey: .read, flag: false)
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleReadForCurrentArticle() {
|
func toggleReadForCurrentArticle() {
|
||||||
if let article = currentArticle {
|
if let article = currentArticle {
|
||||||
toggleRead(article)
|
toggleRead(article)
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleRead(_ article: Article) {
|
func toggleRead(_ article: Article) {
|
||||||
guard !article.status.read || article.isAvailableToMarkUnread else { return }
|
guard !article.status.read || article.isAvailableToMarkUnread else { return }
|
||||||
markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read)
|
markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read)
|
||||||
articlesWithManuallyChangedReadStatus.insert(article)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleStarredForCurrentArticle() {
|
func toggleStarredForCurrentArticle() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user