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) }