From 1f953f3292fe13b009f548bd087bf7293acf13f3 Mon Sep 17 00:00:00 2001
From: Brent Simmons
Date: Sun, 27 Feb 2022 21:56:22 -0800
Subject: [PATCH 1/6] Update appcast for 6.1b4.
---
Appcasts/netnewswire-beta.xml | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml
index d098b9293..4f7b01376 100755
--- a/Appcasts/netnewswire-beta.xml
+++ b/Appcasts/netnewswire-beta.xml
@@ -6,7 +6,17 @@
Most recent NetNewsWire changes with links to updates.
en
- -
+
-
+ NetNewsWire 6.1b4
+ Fixed a few font and sizing issues.
+ ]]>
+ Sun, 27 Feb 2022 21:50:00 -0800
+
+ 10.15.0
+
+
+ -
NetNewsWire 6.1b3
Two new themes: Hyperlegible and NewsFax
From 477062c75672a612ee46f82301c74060c2cb8a47 Mon Sep 17 00:00:00 2001
From: Matt Meissner
Date: Sun, 13 Feb 2022 22:10:36 -0600
Subject: [PATCH 2/6] Attempt #2: Update `gyb` to latest version and use
python3
---
Shared/Secrets.swift.gyb | 6 ++--
Vendor/gyb | 2 +-
Vendor/gyb.py | 62 +++++++++++++++++++++-------------------
3 files changed, 37 insertions(+), 33 deletions(-)
diff --git a/Shared/Secrets.swift.gyb b/Shared/Secrets.swift.gyb
index e51700f4a..5b9602b8a 100644
--- a/Shared/Secrets.swift.gyb
+++ b/Shared/Secrets.swift.gyb
@@ -8,14 +8,14 @@ def chunks(seq, size):
return (seq[i:(i + size)] for i in range(0, len(seq), size))
def encode(string, salt):
- bytes = string.encode("UTF-8")
- return [ord(bytes[i]) ^ salt[i % len(salt)] for i in range(0, len(bytes))]
+ bytes_ = string.encode("UTF-8")
+ return [bytes_[i] ^ salt[i % len(salt)] for i in range(0, len(bytes_))]
def snake_to_camel(snake_str):
components = snake_str.split('_')
return components[0].lower() + ''.join(x.title() for x in components[1:])
-salt = [ord(byte) for byte in os.urandom(64)]
+salt = [byte for byte in os.urandom(64)]
}%
import Secrets
diff --git a/Vendor/gyb b/Vendor/gyb
index dece788e5..8206bd8a0 100755
--- a/Vendor/gyb
+++ b/Vendor/gyb
@@ -1,3 +1,3 @@
-#!/usr/bin/env python2.7
+#!/usr/bin/env python3
import gyb
gyb.main()
diff --git a/Vendor/gyb.py b/Vendor/gyb.py
index d9ce0e7b3..a1ff11ee4 100644
--- a/Vendor/gyb.py
+++ b/Vendor/gyb.py
@@ -4,17 +4,21 @@
from __future__ import print_function
+import io
import os
import re
import sys
-try:
- from cStringIO import StringIO
-except ImportError:
- from io import StringIO
import textwrap
import tokenize
from bisect import bisect
+
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+
try:
basestring
except NameError:
@@ -48,7 +52,7 @@ def split_lines(s):
If the lines are later concatenated, the result is s, possibly
with a single appended newline.
"""
- return [l + '\n' for l in s.split('\n')]
+ return [line + '\n' for line in s.split('\n')]
# text on a line up to the first '$$', '${', or '%%'
@@ -396,9 +400,9 @@ class ParseContext(object):
def __init__(self, filename, template=None):
self.filename = os.path.abspath(filename)
if sys.platform == 'win32':
- self.filename = self.filename.replace('\\', '/')
+ self.filename = '/'.join(self.filename.split(os.sep))
if template is None:
- with open(filename) as f:
+ with io.open(os.path.normpath(filename), encoding='utf-8') as f:
self.template = f.read()
else:
self.template = template
@@ -733,8 +737,10 @@ class Code(ASTNode):
result_string = None
if isinstance(result, Number) and not isinstance(result, Integral):
result_string = repr(result)
- else:
+ elif isinstance(result, Integral) or isinstance(result, list):
result_string = str(result)
+ else:
+ result_string = StringIO(result).read()
context.append_text(
result_string, self.filename, self.start_line_number)
@@ -745,7 +751,7 @@ class Code(ASTNode):
s = indent + 'Code: {' + source_lines[0] + '}'
else:
s = indent + 'Code:\n' + indent + '{\n' + '\n'.join(
- indent + 4 * ' ' + l for l in source_lines
+ indent + 4 * ' ' + line for line in source_lines
) + '\n' + indent + '}'
return s + self.format_children(indent)
@@ -760,8 +766,8 @@ def expand(filename, line_directive=_default_line_directive, **local_bindings):
>>> # manually handle closing and deleting this file to allow us to open
>>> # the file by its name across all platforms.
>>> f = NamedTemporaryFile(delete=False)
- >>> f.write(
- ... r'''---
+ >>> _ = f.write(
+ ... br'''---
... % for i in range(int(x)):
... a pox on ${i} for epoxy
... % end
@@ -800,7 +806,7 @@ def expand(filename, line_directive=_default_line_directive, **local_bindings):
>>> f.close()
>>> os.remove(f.name)
"""
- with open(filename) as f:
+ with io.open(filename, encoding='utf-8') as f:
t = parse_template(filename, f.read())
d = os.getcwd()
os.chdir(os.path.dirname(os.path.abspath(filename)))
@@ -1133,16 +1139,6 @@ def execute_template(
def main():
- """
- Lint this file.
- >>> import sys
- >>> gyb_path = os.path.realpath(__file__).replace('.pyc', '.py')
- >>> sys.path.append(os.path.dirname(gyb_path))
- >>> import python_lint
- >>> python_lint.lint([gyb_path], verbose=False)
- 0
- """
-
import argparse
import sys
@@ -1215,12 +1211,12 @@ def main():
help='''Bindings to be set in the template's execution context''')
parser.add_argument(
- 'file', type=argparse.FileType(),
+ 'file', type=str,
help='Path to GYB template file (defaults to stdin)', nargs='?',
- default=sys.stdin)
+ default='-')
parser.add_argument(
- '-o', dest='target', type=argparse.FileType('w'),
- help='Output file (defaults to stdout)', default=sys.stdout)
+ '-o', dest='target', type=str,
+ help='Output file (defaults to stdout)', default='-')
parser.add_argument(
'--test', action='store_true',
default=False, help='Run a self-test')
@@ -1252,15 +1248,23 @@ def main():
sys.exit(1)
bindings = dict(x.split('=', 1) for x in args.defines)
- ast = parse_template(args.file.name, args.file.read())
+ if args.file == '-':
+ ast = parse_template('stdin', sys.stdin.read())
+ else:
+ with io.open(os.path.normpath(args.file), 'r', encoding='utf-8') as f:
+ ast = parse_template(args.file, f.read())
if args.dump:
print(ast)
# Allow the template to open files and import .py files relative to its own
# directory
- os.chdir(os.path.dirname(os.path.abspath(args.file.name)))
+ os.chdir(os.path.dirname(os.path.abspath(args.file)))
sys.path = ['.'] + sys.path
- args.target.write(execute_template(ast, args.line_directive, **bindings))
+ if args.target == '-':
+ sys.stdout.write(execute_template(ast, args.line_directive, **bindings))
+ else:
+ with io.open(args.target, 'w', encoding='utf-8', newline='\n') as f:
+ f.write(execute_template(ast, args.line_directive, **bindings))
if __name__ == '__main__':
From 2ff8fee3080e1b62a9b623c0b65db78e5e110d62 Mon Sep 17 00:00:00 2001
From: Maurice Parker
Date: Tue, 1 Mar 2022 11:14:41 -0600
Subject: [PATCH 3/6] Reload any Container rows that have change disclosure
state since the last rebuild of the Shadow Table. Fixes #3484
---
iOS/MasterFeed/MasterFeedViewController.swift | 8 +++++
iOS/MasterFeed/ShadowTableChanges.swift | 9 ++++-
iOS/SceneCoordinator.swift | 34 +++++++++++++++++--
3 files changed, 47 insertions(+), 4 deletions(-)
diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift
index 6e2964c2d..4e71c23ae 100644
--- a/iOS/MasterFeed/MasterFeedViewController.swift
+++ b/iOS/MasterFeed/MasterFeedViewController.swift
@@ -627,6 +627,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
}
+ if let rowChanges = changes.rowChanges {
+ for rowChange in rowChanges {
+ if let reloads = rowChange.reloadIndexPaths, !reloads.isEmpty {
+ tableView.reloadRows(at: reloads, with: .none)
+ }
+ }
+ }
+
completion?()
}
diff --git a/iOS/MasterFeed/ShadowTableChanges.swift b/iOS/MasterFeed/ShadowTableChanges.swift
index dd8a12801..49c4a9f9f 100644
--- a/iOS/MasterFeed/ShadowTableChanges.swift
+++ b/iOS/MasterFeed/ShadowTableChanges.swift
@@ -25,6 +25,7 @@ struct ShadowTableChanges {
var section: Int
var deletes: Set?
var inserts: Set?
+ var reloads: Set?
var moves: Set?
var isEmpty: Bool {
@@ -41,15 +42,21 @@ struct ShadowTableChanges {
return inserts.map { IndexPath(row: $0, section: section) }
}
+ var reloadIndexPaths: [IndexPath]? {
+ guard let reloads = reloads else { return nil }
+ return reloads.map { IndexPath(row: $0, section: section) }
+ }
+
var moveIndexPaths: [(IndexPath, IndexPath)]? {
guard let moves = moves else { return nil }
return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) }
}
- init(section: Int, deletes: Set?, inserts: Set?, moves: Set?) {
+ init(section: Int, deletes: Set?, inserts: Set?, reloads: Set?, moves: Set?) {
self.section = section
self.deletes = deletes
self.inserts = inserts
+ self.reloads = reloads
self.moves = moves
}
diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift
index bed102e59..278e0ba9f 100644
--- a/iOS/SceneCoordinator.swift
+++ b/iOS/SceneCoordinator.swift
@@ -87,8 +87,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
+ // Which Containers are expanded
private var expandedTable = Set()
+
+ // Which Containers used to be expanded. Reset by rebuilding the Shadow Table.
+ private var lastExpandedTable = Set()
+
+ // Which Feeds have the Read Articles Filter enabled
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
+
+ // Flattened tree structure for the Sidebar
private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
private(set) var preSearchTimelineFeed: Feed?
@@ -719,8 +727,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
rebuildBackingStores()
}
+ /// This is a special function that expects the caller to change the disclosure arrow state outside this function.
+ /// Failure to do so will get the Sidebar into an invalid state.
func expand(_ node: Node) {
guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return }
+ lastExpandedTable.insert(containerID)
expand(containerID)
}
@@ -742,8 +753,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
clearTimelineIfNoLongerAvailable()
}
+ /// This is a special function that expects the caller to change the disclosure arrow state outside this function.
+ /// Failure to do so will get the Sidebar into an invalid state.
func collapse(_ node: Node) {
guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return }
+ lastExpandedTable.remove(containerID)
collapse(containerID)
}
@@ -1548,9 +1562,10 @@ private extension SceneCoordinator {
currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject)
}
- // Compute the differences in the shadow table rows
+ // Compute the differences in the shadow table rows and the expanded table entries
var changes = [ShadowTableChanges.RowChanges]()
-
+ let expandedTableDifference = lastExpandedTable.symmetricDifference(expandedTable)
+
for (section, newSectionRows) in newShadowTable.enumerated() {
var moves = Set()
var inserts = Set()
@@ -1576,9 +1591,22 @@ private extension SceneCoordinator {
}
}
- changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, moves: moves))
+ // We need to reload the difference in expanded rows to get the disclosure arrows correct when programmatically changing their state
+ var reloads = Set()
+
+ for (index, newFeedNode) in newSectionRows.feedNodes.enumerated() {
+ if let newFeedNodeContainerID = (newFeedNode.node.representedObject as? Container)?.containerID {
+ if expandedTableDifference.contains(newFeedNodeContainerID) {
+ reloads.insert(index)
+ }
+ }
+ }
+
+ changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, reloads: reloads, moves: moves))
}
+ lastExpandedTable = expandedTable
+
// Compute the difference in the shadow table sections
var moves = Set()
var inserts = Set()
From 0719e5883b3c23f38a3aacdcbbdefdd0ef5389d3 Mon Sep 17 00:00:00 2001
From: Maurice Parker
Date: Tue, 1 Mar 2022 14:12:43 -0600
Subject: [PATCH 4/6] Clear the timeline when disclosing a web feed so that the
previously loaded timeline articles aren't merged. Fixes #3485
---
iOS/SceneCoordinator.swift | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift
index 278e0ba9f..2b63e3199 100644
--- a/iOS/SceneCoordinator.swift
+++ b/iOS/SceneCoordinator.swift
@@ -1151,7 +1151,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
rebuildBackingStores(initialLoad: initialLoad, completion: {
self.treeControllerDelegate.resetFilterExceptions()
- self.selectFeed(webFeed, animations: animations, completion: completion)
+ self.selectFeed(nil) {
+ self.selectFeed(webFeed, animations: animations, completion: completion)
+ }
})
}
From f22239db361817efb837817a1bc65c1381dc189b Mon Sep 17 00:00:00 2001
From: Maurice Parker
Date: Tue, 1 Mar 2022 14:43:54 -0600
Subject: [PATCH 5/6] Change task completion notification so that it blocks
until NNW has completed suspending. Fixes #3200
---
iOS/AppDelegate.swift | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift
index f7cbe5b84..aeb74260e 100644
--- a/iOS/AppDelegate.swift
+++ b/iOS/AppDelegate.swift
@@ -410,11 +410,11 @@ private extension AppDelegate {
// set expiration handler
task.expirationHandler = { [weak task] in
+ os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info)
DispatchQueue.main.sync {
self.suspendApplication()
+ task?.setTaskCompleted(success: false)
}
- os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info)
- task?.setTaskCompleted(success: false)
}
}
From 27dd920cce355a976fa68ca7bf3f4e25b0095196 Mon Sep 17 00:00:00 2001
From: Maurice Parker
Date: Tue, 1 Mar 2022 14:53:43 -0600
Subject: [PATCH 6/6] Change sync to async
---
iOS/AppDelegate.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift
index 283dbf7ea..2a29531c4 100644
--- a/iOS/AppDelegate.swift
+++ b/iOS/AppDelegate.swift
@@ -409,7 +409,7 @@ private extension AppDelegate {
// set expiration handler
task.expirationHandler = { [weak task] in
os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info)
- DispatchQueue.main.sync {
+ DispatchQueue.main.async {
self.suspendApplication()
task?.setTaskCompleted(success: false)
}