I had the fun of interviewing old friend Daniel Jalkut on the latest episode of The Omni Show.
"},"alternate":[{"href":"https://inessential.com/2019/09/25/i_had_the_fun_of_interviewing_old_friend","type":"text/html"}],"crawled":1569829821629,"published":1569437386000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/13/netnewswire_5_0_1_released","fingerprint":"f53acc86","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d995:18991ffa","summary":{"direction":"ltr","content":"\nNetNewsWire 5.0.1 is almost entirely a bug-fix release — see the release notes for the full scoop.
\nIt includes one sort-of new feature: there’s now a checkbox in Preferences for turning off the unread count in the Dock. (It was a hidden pref — now it’s visible.)
\nStatus
\nHere’s what else we’re working on:
\n- \n
- iOS/iPadOS app \n
- NetNewsWire 5.0.2 for Mac — which will mainly be about performance (yes, we can make it even faster) \n
- NetNewsWire 5.1 for Mac — tentative feature list includes content extraction and at least one more syncing option (but we might change our minds on these: anything can happen between now and then) \n
We might also distribute NetNewsWire 5.0.2 for Mac on the Mac App Store. No guarantees yet, of course, but work is happening in that direction. This goes to our goal of getting as many people as possible using RSS readers.
"},"alternate":[{"href":"https://inessential.com/2019/09/13/netnewswire_5_0_1_released","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5.0.1 Released","published":1568408217000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/10/had_to_get_a_new_key_fob_at_work_today_m","fingerprint":"5b6c292f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d994:18991ffa","summary":{"direction":"ltr","content":"Had to get a new key fob at work today — my old one wore out. Just a couple weeks shy of my fifth anniversary at Omni! Time flies.
\nI figure I’m just over eight years from retiring, so I’m not even halfway done here. :)
"},"alternate":[{"href":"https://inessential.com/2019/09/10/had_to_get_a_new_key_fob_at_work_today_m","type":"text/html"}],"crawled":1569829821629,"published":1568153137000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/06/on_syncing_netnewswire_using_icloud","fingerprint":"3b5ade1b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d993:18991ffa","summary":{"direction":"ltr","content":"People have been asking me about supporting iCloud as a sync method for NetNewsWire.
\nIt would be really cool because:
\n- \n
- There’s no sign-in \n
- It’s free — no need to spend money on another service \n
- It would help broaden the pool of people using RSS, since there would be no additional expense or service they’d need — they could just get going \n
It’s a great idea — no question. Given that my goal is to get as many people as possible using RSS, this makes total sense.
\nWhy we didn’t ship with this feature
\nFor the first release — I still think of it as a 1.0, because it really is — our best bet was to appeal to people already using an existing RSS service. We know that those people like and use RSS, and they’re the people most likely to check out a new RSS app.
\n(We could have delayed and shipped with support for more existing services, but we figured one was enough to get started with, and we could add other services later. And we are.)
\nIn other words, we tried to make an app that the existing market would like. And that’s the right call when you’re starting out.
\nAlso: iCloud sync makes the most sense when you have both a Mac and an iOS app, and we don’t — the iOS app is still in progress. We totally expect people to use NetNewsWire on the Mac and Unread or Reeder on their iPhone and iPad — and iCloud sync won’t work across apps. This scenario requires using services such as Feedbin.
\nWhy I have no idea when this feature might appear
\nFor any existing RSS service, we can be confident that our effort to support it in NetNewsWire would be successful. This is well-trodden ground: we make some web API calls, integrate with our database, and done. It’s not nothing, but conceptually it’s simple and there’s no cause to worry about technical issues.
\nBut iCloud syncing will mean writing exploratory code and only then finding out if it’s going to work.
\nSyncing the feeds list should be relatively easy — the real issue is with syncing read/unread/starred states of articles. That means a lot of small records.
\nIs CloudKit up to this? What are the limits? How fast is it? How reliable?
\nWe just don’t know.
\nYes, it’s encouraging that News Explorer has this feature — but that doesn’t tell us much about the limits, reliability, and performance.
\nWorking on this is a risk.
\nSo — as you can imagine — we’re still more keen on supporting existing RSS services, because we know there are plenty of people who for-sure like RSS, and who might like NetNewsWire, but who won’t switch their syncing system just to use NetNewsWire.
\nThat said: I do think we’ll get around to trying this, and I’ll be super-pleased if it works, because it really is a great idea — but we have a bunch of other work to do first. (Including the iOS app!)
"},"alternate":[{"href":"https://inessential.com/2019/09/06/on_syncing_netnewswire_using_icloud","type":"text/html"}],"crawled":1569829821629,"title":"On Syncing NetNewsWire Using iCloud","published":1567817061000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/06/_markos_charatzas_writes_https_qnoid_com","fingerprint":"ca200f5a","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d992:18991ffa","summary":{"direction":"ltr","content":"Markos Charatzas writes about his excitement in joining the Apple developer world in 2009 to his eventual disillusionment today.
"},"alternate":[{"href":"https://inessential.com/2019/09/06/_markos_charatzas_writes_https_qnoid_com","type":"text/html"}],"crawled":1569829821629,"published":1567788970000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/04/on_the_many_netnewswire_feature_requests","fingerprint":"66196df9","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d991:18991ffa","summary":{"direction":"ltr","content":"A number of people have asked that NetNewsWire show the full web page — right there, in the app — after clicking a link.
\nThe idea is pretty good! It solves two big problems:
\n- \n
- You get full content, which is great when a feed contains only summaries or truncated articles \n
- You don’t have to switch to another app: you can stay right where you are \n
You’d think it’s a no-brainer, and we should just go ahead. But there are other considerations.
\nOne big one is that your ad blockers and privacy extensions won’t run. They work in Safari, but they do not extend to other apps that use WebKit. This means that viewing a web page in NetNewsWire would be less secure and more annoying than viewing the same page in Safari (or whatever your browser is).
\nThis points to one of my design principles: the app should have boundaries. Some features belong in the app, and some features are best left to apps that do that feature way better than NetNewsWire could. One of those things is showing web pages — that’s really a web browser feature.
\nHaving boundaries means we can concentrate on doing a great job at the things that do belong in the app.
\n(Before you mention SFSafariViewController, recall that it’s iOS-only.)
\nWhat about the glory days?
\n“But Brent! In NetNewsWire 2.0 you added a tabbed browser to NetNewsWire, and it was awesome and a hugely popular feature!”
\nIt was! But times have changed. Many websites are hostile these days. In 2005, this feature was fine — but these days it’s totally not.
\nA winged messenger arrives with a solution
\nThere is a solution to the problem of showing full content and not leaving the app, and it’s a feature that really does belong in an RSS reader: using content extraction to grab the article from the original page.
\nIf you’ve ever used Safari’s Reader view, then you know what I’m talking about. The idea is that NetNewsWire would do something very much like the Reader view (but inline, in the article pane), that grabs the content and formats it nicely, without all the extra junk that is not the article you want to read.
\nThere are a number of open source options for this. We’re looking at using Feedbin’s content extraction service (which wouldn’t require you to have a Feedbin account).
\nThe generous folks at Feedbin are running a copy of the open-source Mercury Parser, and they’ve offered to open this service up to RSS readers like NetNewsWire. (Reeder uses it already, for instance.)
\nWhen?
\nRight now we’re working on NetNewsWire 5.0.1, which is (almost entirely) a bug-fix release. I don’t know what’s going to be in 5.1 yet — we’re still digesting all the feedback, looking at our original roadmap, and thinking about things.
\nWe’re also working on NetNewsWire for iOS! We’re busy.
\nBut this is definitely the kind of feature that should come sooner rather than later.
"},"alternate":[{"href":"https://inessential.com/2019/09/04/on_the_many_netnewswire_feature_requests","type":"text/html"}],"crawled":1569829821629,"title":"On the Many NetNewsWire Feature Requests to Show Full Web Pages","published":1567661107000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/02/on_my_funny_ideas_about_what_beta_means","fingerprint":"bb260103","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d990:18991ffa","summary":{"direction":"ltr","content":"John Gruber has mentioned, on The Talk Show, that I’ve got some weird ideas about what beta means.
\nHere are my definitions:
\ndevelopment (d): everything is in progress and the app might be completely unusable.
\nalpha (a): the app is feature-complete and has no known bugs — but, importantly, it’s had very little testing.
\nbeta (b): the app is feature-complete, has no known bugs, and has been tested — but further testing is still warranted. Every beta is a release candidate.
\nThese are defined in a NetNewsWire Technote. It’s important to have definitions that everybody working on or testing the app understands.
\nBut why these rather strict definitions?
\nIt’s part of our commitment to quality. What matters is the end result — the shipping app — and these definitions make sure we don’t get to beta, or even alpha, with the app up on the table with wires sticking out and pieces missing.
\nThis gives us a big space between development and shipping, and that space is all about making sure the bugs are all fixed.
\nThis is a matter of ethics and pride in our work. Absolutely.
\nBut it’s also pragmatic. This is an open source app, written by volunteers in their spare time, and having this rhythm baked-in to the process helps make sure we can uphold our standards even without full-time developers, managers, and testers.
\n* * *
\nAnd… it bugs me how little real attention our industry pays to quality these days. In some cases the consequences are disastrous; in other cases they’re merely expensive. It doesn’t have to be this way.
\nIf it seems like I’m going too far with my definitions, well, I’m trying to bend the stick here.
"},"alternate":[{"href":"https://inessential.com/2019/09/02/on_my_funny_ideas_about_what_beta_means","type":"text/html"}],"crawled":1569829821629,"title":"On My Funny Ideas About What Beta Means","published":1567455823000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/31/i_love_this_netnewswire_write_up_on_wp_t","fingerprint":"e526eb19","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98f:18991ffa","summary":{"direction":"ltr","content":"I love this NetNewsWire write-up on WP Tavern.
"},"alternate":[{"href":"https://inessential.com/2019/08/31/i_love_this_netnewswire_write_up_on_wp_t","type":"text/html"}],"crawled":1569829821629,"published":1567280353000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/31/netnewswire_5_feature_requests","fingerprint":"57ea983b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98e:18991ffa","summary":{"direction":"ltr","content":"NetNewsWire 5.0 is a 1.0 app in disguise.
\nAnd so, as expected, we’ve had a ton of feature requests. Most people tend to request one or two features — and there’s a huge variety in these. People want different things.
\nNevertheless, there are a few themes we can pick out from what people are asking for:
\n- \n
- More syncing options, especially Feedly support \n
- iOS app \n
- Some way to deal with partial-content feeds \n
- Customization of the article pane (fonts, colors, etc.) \n
- Traditional view (timeline on top with single lines, article below) \n
- More sharing options (Instapaper, Pinboard, etc.) \n
- Customizable keyboard shortcuts \n
- State restoration \n
- Localizations \n
- Hiding read items in the timeline (or dimming them) \n
- Hiding feeds (in the sidebar) that have no unread articles \n
- User-created smart feeds \n
The less-common, more singular requests are for things like specific sorting options — there are lots of different small options that people would like.
\nPeople have also asked for things that might surprise you (they surprised me) — for instance, we’ve had a request for monochrome icons for the toolbar. Another request for a Dark Mode that’s different from Apple’s Dark Mode. Etc.
\nHow We Choose What To Do Next
\nThe first principle is that we can’t lose what we love about the app. We do our damnedest to ship with no bugs, and the app needs to be fast and, most importantly, it needs to feel lighter-than-air.
\nWhenever you add things — even if the app remains just as fast, even if there are no bugs — you still run the risk of losing that feeling of lightness. One of the quickest ways to lose that feeling is to add a whole bunch of preferences, View menu options, toolbar commands, and other chrome. So we’re going to be very slow to add things like that.
\nNetNewsWire needs to not become fiddly. (Earlier versions of NetNewsWire got way too fiddly.)
\nThere are other questions we ask about a feature before we do it.
\n- \n
- Will it substantially benefit current users? \n
- Will it bring a number of new users to the app? \n
- Does the feature depend on something else being done first? \n
- How much work will it take? \n
- Does it require resources (such as new icons) that our programmers can’t provide? \n
- Does the feature really belong in an RSS reader at all? \n
And, because this is an open source app, there’s another dimension: people. Is someone available? Has someone just shown up who’s eager to work on a specific feature? Those things have an impact on scheduling, too.
\nThe good news is that most of the common feature requests are obvious things to do.
\nSome examples — not nearly everything, just a few thoughts:
\nThe iOS app is in progress. Maurice Parker has been writing it, and it’s coming along very well. Still plenty more to do, and we won’t ship before iOS 13 ships, but it’s happening.
\nAdding syncing options is a definite good thing for the app. Doing the first one (Feedbin) was the big effort, because it required building the infrastructure that makes syncing possible. Once that was done, adding additional services is not super-difficult. (Not easy, no. Nothing’s trivial. But at least the infrastructure and patterns are in place.)
\nWe’d like to support all the various services, or at least a majority of them. And we have people working on adding services.
\nCustomization of the article pane will most likely work the way it did in older versions of NetNewsWire: we had theme files which included templates and CSS. The app shipped with a few, and you could make your own and use themes other people made.
\nThis feature shipped with NetNewsWire 2.0, and people really loved it. It was fun!
\nMore sharing options is an obvious good idea. Of course you should be able to send to Instapaper, Pocket, Pinboard, and so on. We shipped with custom support for MarsEdit and the Micro.blog app — mainly because I use those apps. But an RSS reader ought to support as many sharing workflows as possible. That’s one of the core points of the app.
\n* * *
\nAnyway — the above doesn’t cover everything. Don’t take any of the above as gospel about what we’re doing or when, or what we’re not doing. We haven’t planned 5.1 yet! It’s too soon.
\nThere are also features that we want to do that people haven’t asked for, but that we think are cool. \uD83C\uDFB8
\nThe take-away from this article should be: we’re being very careful about designing and implementing new features, because we have to make sure NetNewsWire doesn’t lose what makes it special.
\nBut we are doing new features, because there are so many things that can make the app even better — we can make it better for current users and we can bring in new users.
"},"alternate":[{"href":"https://inessential.com/2019/08/31/netnewswire_5_feature_requests","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5 Feature Requests","published":1567278518000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/29/follow_through","fingerprint":"444937b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98d:18991ffa","summary":{"direction":"ltr","content":"Decades ago, when I was working for Dave Winer at UserLand, I learned about the concept of follow-through after a major release.
\nIf you’re an app maker, it might seem like your goal is to get to release day. Get the app done, make it available, publish an announcement, and then get back to coding. Let the world do what it’s going to do.
\nOne bang, and then back to work, in other words.
\nBut that’s not going to maximize your chances for a good release. You need to follow through — you need to keep going.
\nSome of the things you might do, in no particular order:
\n- \n
- Publish tips on using your app — one a day or so \n
- Update your website with feedback, testimonials, and good reviews \n
- Be available and communicative about your app \n
- Go on some podcasts \n
- Write about how release day went \n
- Write about plans for the x.0.1 version \n
- Field bug reports and feature requests gratefully \n
- Thank reviewers who’ve done a good job \n
- Make it as easy as possible for reporters and reviewers to get access to your app and to you \n
- Work to build a community of customers, on Slack or similar \n
I’m sure you can think of more things to do — the above isn’t everything, and every app is different.
\nBut the key is that you don’t just do the release and then stop. Instead, show that you‘re responsive, show that your app has momentum, show that you care enough to keep showing up.
\nFor me, at least, this is the fun part. I realize that’s not true for everybody — but you should do it anyway. \uD83C\uDFA9
"},"alternate":[{"href":"https://inessential.com/2019/08/29/follow_through","type":"text/html"}],"crawled":1569829821629,"title":"Follow-Through","published":1567110304000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/28/daniel_figures_out_one_of_the_two_crashi","fingerprint":"669e65c4","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98c:18991ffa","summary":{"direction":"ltr","content":"We have a few reports of a crash where the add-feed-sheet window doesn’t load. There’s a line of code with window! — because of course we expect the window to have been loaded — and it crashes right there.
This crash made zero sense to me, but Daniel Jalkut figured out the most likely cause and was able to reproduce it: it’s because the person has moved the app (from one folder to another) after launching it, while it’s running, and the nib-loading machinery can’t find the nib, because it’s moved along with the app.
\nTip: if you’re going to move an app, quit it first, then move it, and then re-launch it!
\nAt any rate: our fix for this will be to load that sheet on startup, and then recycle it on each use. This fix will go into NetNewsWire 5.0.1.
\nThis just fixes the bug with this one nib, though. A more systematic fix — maybe just a warning to the user suggesting they quit and re-launch — would be a good idea.
\nFile under “bugs iOS developers never have to worry about.” \uD83D\uDC07
\nPS We have a 5.0.1 beta milestone now.
"},"alternate":[{"href":"https://inessential.com/2019/08/28/daniel_figures_out_one_of_the_two_crashi","type":"text/html"}],"crawled":1569829821629,"title":"Daniel Figures Out One of the Two Crashing Bugs","published":1567022746000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/27/how_release_day_went","fingerprint":"2394a816","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98b:18991ffa","summary":{"direction":"ltr","content":"Yesterday was a great day! A few things to note, in no particular order:
\nNetNewsWire got some press coverage, including a well-done review in MacStories.
\nWe got a lot of feature requests, but no bug reports.
\nExcept that we did get a single-digit number of crash logs. On investigation, I found two distinct backtraces — we’ll need to fix those. The thing is, there’s no freakin’ way the app should crash in those spots. Except that, obviously, it can. Rarely, but it happens.
\nThe servers started timing-out at one point during the day. I contacted DreamHost support and they fixed things (and told me that the fixes they applied should prevent this in the future).
\nThere were a number of nice blog posts and tweets about NetNewsWire, which was awesome. After working so hard for so long, it’s great when people appreciate the app. We don’t get paid in money, after all. \uD83D\uDC23
\nI have no idea how many downloads of the app there were. GitHub is hosting the download, via its releases feature, and I don’t see a way to find out how many times it’s been downloaded. Which is totally fine with me.
\n* * *
\nI should say something more about the no-bug-reports. There’s no special magic or talent or anything to this — there’s just the willingness to say that we’re not going to ship until we’ve got the bugs out, and then sticking to that.
\nThis is a matter of pride and ethics, for sure, but there’s another dimension: since the app is open source, it’s written by volunteers (including me), and we have no dedicated support team. Any time we spend fielding bug reports is time taken away from working on the next feature.
\nMaking apps — even, or especially, free apps — is an exercise in economics. With free apps, the economics are even more constrained, because nobody is going to hire even a part-time support person. So we do everything we can do keep costs down — especially time costs.
\nPlus — buggy apps can be demoralizing to the people who work on them. Part of my job is to make sure people are proud and happy to work on the app. And that means making sure everyone knows we’re super-serious about doing our best to never ship bugs.
"},"alternate":[{"href":"https://inessential.com/2019/08/27/how_release_day_went","type":"text/html"}],"crawled":1569829821629,"title":"How Release Day Went","published":1566937707000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/26/netnewswire_5_0_now_available","fingerprint":"175d2cdb","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98a:18991ffa","summary":{"direction":"ltr","content":"\nNetNewsWire 5.0 is shipping!
\nIn case you haven’t been following along until just now: NetNewsWire is an open source RSS reader for Mac. It’s free! You can just download it and use it. No strings.
\nIt’s designed to be stable, fast, and free of bugs. It doesn’t have a lot of features yet, and that’s because we prioritized quality over features. We will be adding more features, of course, but not quickly. We’re also working on an iOS app.
\nIt syncs using Feedbin. We’ll support more systems in the future (as many as possible).
\nI hope you like it!
\nSome links…
\n- \n
- NetNewsWire home page \n
- Screenshots \n
- NetNewsWire blog \n
- Frequently Asked Questions \n
- NetNewsWire Help \n
- NetNewsWire History \n
- NetNewsWire Slack group \n
- How to Support NetNewsWire \n
- NetNewsWire repository on GitHub \n
Thanks to so many people
\nI want to especially thank Sheila Simmons and my family and friends.
\nThis release took five years to make, and for four of those years it wasn’t even called NetNewsWire. It was just a year ago that I got the name NetNewsWire back from Black Pixel — and I thank them again for their wonderful generosity.
\nI also want to thank Brad Ellis for making the beautiful app icon and toolbar icons. Thanks to our major code contributors: Maurice Parker, Olof Hellman, and Daniel Jalkut. Thanks to Ryan Dotson for writing the Help book. Thanks to Joe Heck for looking after infrastructure issues (especially continuous integration).
\nThanks to my co-workers and friends at The Omni Group (which is a wonderful place to work). Thanks to the ever-patient and ever-awesome NetNewsWire beta testers on the Slack group and elsewhere.
\nAnd thanks to everyone who’s ever used the app in its 17-years-and-counting run. Because of you, NetNewsWire has been, and remains, the thrill of my career.
"},"alternate":[{"href":"https://inessential.com/2019/08/26/netnewswire_5_0_now_available","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5.0 Now Available","published":1566834451000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/22/end_of_the_line_for_netnewswire_3_3_2","fingerprint":"e30daaa8","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d989:18991ffa","summary":{"direction":"ltr","content":"This is a little bit of bad news. It’s not my intention, and it’s not what I want to happen — but NetNewsWire 3.3.2 apparently does not launch in the next version of macOS (10.15, Catalina).
\nIt links to the PubSub framework, which is not included with the next macOS.
\nNetNewsWire 3.3.2 was the last release of the full version that I worked on, before selling NetNewsWire to Black Pixel, and I’ve heard from lots of people that they’ve been using it ever since. They never switched.
\nI would rather it continued working forever, but that’s not to be. Not my choice. Sorry about that!
"},"alternate":[{"href":"https://inessential.com/2019/08/22/end_of_the_line_for_netnewswire_3_3_2","type":"text/html"}],"crawled":1569829821629,"title":"End of the Line for NetNewsWire 3.3.2","published":1566515704000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/21/the_netnewswire_blog_has_the_details_on_","fingerprint":"c583e740","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d988:18991ffa","summary":{"direction":"ltr","content":"The NetNewsWire blog has the details on NetNewsWire 5.0b5 — which should be the last beta.
\nStill planning to do the 5.0 final release Monday morning, which really means doing the release on Sunday and pushing an announcement to this blog Monday morning. :)
\nThe last things on my to-do list are actually writing that announcement and doing screenshots for the NetNewsWire web page. Easy. \uD83D\uDC2F
"},"alternate":[{"href":"https://inessential.com/2019/08/21/the_netnewswire_blog_has_the_details_on_","type":"text/html"}],"crawled":1569829821629,"published":1566452581000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/20/immunization","fingerprint":"39a4bdb0","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d987:18991ffa","summary":{"direction":"ltr","content":"Before every major release I like to try and think of everything mean that people might say about the app. It’s fun!
\nSo we just went through this exercise on the NetNewsWire Slack group. Here’s a taste:
\n- \n
- This took five years? I could write an RSS parser in a weekend. \n
- Can’t get my Twitter and Facebook feeds. Whatever. \n
- Doesn’t work with my Usenet host. \n
- The information density of the timeline is… lacking. What the hell. \n
- Not truly open source since it’s on a Mac. \n
- Not truly open source since it’s not GPL. \n
- No vim keys. Why bother. \n
- Regular people will never use an RSS reader. What’s the point? \n
- Brent’s last good idea was in 2002. Consider this a textbook case of coasting. \n
- Great app. Too bad RSS died with Google Reader. \n
- It totally didn’t pick up my subscriptions from the earlier version. How is this an upgrade? \n
- When does a 5.0 have fewer features than a 3.0? When it’s NetNewsWire. \n
- The echo chamber will love this app. They always do. \n
- Free app. Continues the race to the bottom. Pour one out for Silvio Rizzi. \n
- No way to send to Instapaper. Fuck it. \n
- Brent Simmons can’t stop pursuing a technology that even Google famously admitted was not worth bothering with. \n
- If this app took five years, imagine how long it will take before it will actually sync with Feedly. \n
- Sure it’s free, but I bet the Feedbin people paid them off, because the only way to sync is to pay money to Feedbin. \n
- No iCloud sync? Jerks. \n
- No iOS app. The revolution happened on mobile, Brant. What the actual fuck. \n
- Shoulda been Catalyst. Dinosaurs wrote this app. \n
- Not on the Mac App Store? I guess they don’t want users. \n
- I would totally use this if it had just this one [feature x], which I can’t believe they shipped without. (Multiply this comment by 100, with a different feature x each time.) \n
- Area Man Can’t Let RSS Go \n
Some feedback will be factually inaccurate, but we like to imagine that too:
\n- \n
- I remember using NetNewsWire on OS 9, and it hasn’t really improved since then. They should make it a Cocoa app. \n
- Doesn’t work with web comics. POS \n
- Doesn’t support 10.5. \n
- It should be free. \n
- You’d think they would have updated the design — but it looks exactly like NetNewsWire of old. \n
- Why the hell would they build on that aging code base from Black Pixel? I heard it doesn’t even use ARC. \n
- No way to sync? What’s their actual problem? \n
See? The actual feedback will be nicer than the stuff we thought up. This provides a bit of immunization. :)
\nBut, also, there will be negative feedback we didn’t imagine. That’s the gold!
\n* * *
\nBonus from Daniel Jalkut, but not actually a criticism:
\n\n"},"alternate":[{"href":"https://inessential.com/2019/08/20/immunization","type":"text/html"}],"crawled":1569829821629,"title":"Immunization","published":1566332363000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/19/i_think_were_still_on_track_for_releasin","fingerprint":"592043f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d986:18991ffa","summary":{"direction":"ltr","content":"Can’t innovate, my RSS.
\n
I think we’re still on track for releasing NetNewsWire 5.0 Monday, August 26. There will be one more beta before then.
\nI’ll be available for podcasts, interviews-via-email, etc. If you’d like to set something up, email me or DM me on Twitter.
"},"alternate":[{"href":"https://inessential.com/2019/08/19/i_think_were_still_on_track_for_releasin","type":"text/html"}],"crawled":1569829821629,"published":1566259329000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]}]} \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/AddFeed/feedly_collections_addfeed.json b/Frameworks/Account/AccountTests/Feedly/AddFeed/feedly_collections_addfeed.json deleted file mode 100644 index bd718c0cb..000000000 --- a/Frameworks/Account/AccountTests/Feedly/AddFeed/feedly_collections_addfeed.json +++ /dev/null @@ -1 +0,0 @@ -[{"customizable":true,"feeds":[{"feedId":"feed/http://tidbits.com/feeds/tidbits_blurb.rss","id":"feed/http://tidbits.com/feeds/tidbits_blurb.rss","title":"TidBITS: Apple News for the Rest of Us","updated":1569876918424,"velocity":7.7,"subscribers":1,"website":"https://tidbits.com","language":"en","description":"Thoughtful, detailed coverage of everything Apple for 29 years\nand the TidBITS Content Network for Apple professionals"},{"feedId":"feed/http://www.macalope.com/feed/","id":"feed/http://www.macalope.com/feed/","title":"Macalope","updated":1498941877000,"velocity":0.0,"subscribers":1,"website":"http://www.macalope.com","language":"en","state":"dormant","description":"Full of sound and furry"},{"feedId":"feed/http://flyingmeat.com/blog/atom.xml","id":"feed/http://flyingmeat.com/blog/atom.xml","title":"The Flying Meat Weblog","updated":1343168154000,"velocity":0.0,"subscribers":1,"website":"https://flyingmeat.com/blog/","language":"en","state":"dormant"},{"feedId":"feed/http://www.macdrifter.com/feeds/all.atom.xml","id":"feed/http://www.macdrifter.com/feeds/all.atom.xml","title":"Macdrifter","updated":1561539243000,"velocity":0.1,"subscribers":1,"website":"http://www.macdrifter.com/","language":"en"},{"feedId":"feed/http://9to5mac.com/feed/","id":"feed/http://9to5mac.com/feed/","title":"9to5Mac","updated":1569970519358,"velocity":28.0,"subscribers":2,"website":"https://9to5mac.com","language":"en","description":"Apple News & Mac Rumors Breaking All Day"}],"label":"Macintosh","created":1569829941677,"enterprise":false,"numFeeds":5,"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815"},{"customizable":false,"feeds":[{"feedId":"feed/http://inessential.com/xml/rss.xml","id":"feed/http://inessential.com/xml/rss.xml","title":"Inessential","updated":1569971230061,"velocity":4.5,"subscribers":1,"website":"https://inessential.com/","mustRead":true,"language":"en","description":"Brent Simmons’s weblog."}],"label":"Must Read","created":1569417923847,"enterprise":false,"numFeeds":1,"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must"},{"customizable":true,"feeds":[{"feedId":"feed/http://inessential.com/xml/rss.xml","id":"feed/http://inessential.com/xml/rss.xml","title":"Inessential","updated":1569971230061,"velocity":4.5,"subscribers":1,"website":"https://inessential.com/","mustRead":true,"language":"en","description":"Brent Simmons’s weblog."}],"label":"NewCollection","created":1569918572642,"enterprise":false,"numFeeds":1,"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173"},{"customizable":true,"feeds":[{"feedId":"feed/http://www.codebykevin.com/blosxom.cgi/index.rss","id":"feed/http://www.codebykevin.com/blosxom.cgi/index.rss","title":"Code by Kevin","updated":1564803480000,"velocity":0.1,"subscribers":1,"website":"https://www.codebykevin.com/blosxom.cgi","language":"en","description":"Programming, code, business, and other pursuits"},{"feedId":"feed/http://www.raywenderlich.com/feed","id":"feed/http://www.raywenderlich.com/feed","title":"Ray Wenderlich","updated":1569960327114,"velocity":5.4,"subscribers":1,"language":"en"},{"feedId":"feed/http://ddeville.me/feed.xml","id":"feed/http://ddeville.me/feed.xml","title":"Damien DeVille","updated":1454187600000,"velocity":0.0,"subscribers":1,"website":"http://ddeville.me","language":"en","state":"dormant","description":"Software engineer at Dropbox. Previously at Realmac Software. UCL Computer Science alumnus."},{"feedId":"feed/http://owensd.io/rss.xml","id":"feed/http://owensd.io/rss.xml","title":"owensd.io - thoughts in and out - Articles","updated":1534837458000,"velocity":0.0,"subscribers":1,"website":"https://owensd.io","language":"en","state":"dormant","description":"A builder of things."},{"feedId":"feed/http://prog21.dadgum.com/atom.xml","id":"feed/http://prog21.dadgum.com/atom.xml","title":"Programming in the 21st Century","updated":1483509600000,"velocity":0.0,"subscribers":1,"website":"http://prog21.dadgum.com/","language":"en","state":"dormant"},{"feedId":"feed/http://feeds.feedburner.com/alistapart/main","id":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","updated":1569829877486,"velocity":4.5,"subscribers":1,"website":"https://alistapart.com","language":"en","description":"Articles for people who make web sites."},{"feedId":"feed/http://www.russbishop.net/feed","id":"feed/http://www.russbishop.net/feed","title":"Russ Bishop (atom)","updated":1551122826000,"velocity":0.0,"subscribers":1,"website":"http://www.russbishop.net/feed","language":"en","state":"dormant","description":"This blog represents my own personal opinion and is not endorsed by my employer."},{"feedId":"feed/http://mentalfaculty.tumblr.com/rss","id":"feed/http://mentalfaculty.tumblr.com/rss","title":"The Mental Blog","updated":1468311749000,"velocity":0.0,"subscribers":1,"website":"https://mentalfaculty.tumblr.com/","language":"en","state":"dormant","description":"Drew McCormack (@drewmccormack) is founder of The Mental Faculty, developer of Mental Case and the Ensembles sync framework"},{"feedId":"feed/http://blog.amyworrall.com/rss","id":"feed/http://blog.amyworrall.com/rss","title":"What Amy Did","updated":1486165238000,"velocity":0.0,"subscribers":1,"website":"https://blog.amyworrall.com/","language":"en","state":"dormant","description":"I’m a software developer from Coventry. I care about design and user experience. \n\nFollow me on Twitter: @amyruthworrall"},{"feedId":"feed/http://oleb.net/blog/atom.xml","id":"feed/http://oleb.net/blog/atom.xml","title":"Ole Begemann: iOS Development","updated":1559322747743,"velocity":0.1,"subscribers":1,"website":"https://oleb.net/blog/","language":"en"},{"feedId":"feed/http://subjc.com/atom.xml","id":"feed/http://subjc.com/atom.xml","title":"Subjective-C","updated":1461251700000,"velocity":0.0,"subscribers":2,"website":"http://subjc.com/","language":"en","state":"dormant"},{"feedId":"feed/http://robnapier.net/atom.xml","id":"feed/http://robnapier.net/atom.xml","title":"Cocoaphony","updated":1558929600000,"velocity":0.1,"subscribers":1,"website":"https://robnapier.net/","language":"en"},{"feedId":"feed/http://cocoamanifest.net/feeds/index.xml","id":"feed/http://cocoamanifest.net/feeds/index.xml","title":"Cocoa Manifest","updated":1402642440000,"velocity":0.0,"subscribers":2,"website":"http://cocoamanifest.net","language":"en","state":"dormant"},{"feedId":"feed/http://petersteinberger.com/atom.xml","id":"feed/http://petersteinberger.com/atom.xml","title":"Peter Steinberger","updated":1438088460000,"velocity":0.0,"subscribers":1,"website":"http://petersteinberger.com/","language":"en","state":"dormant"},{"feedId":"feed/http://www.weheartswift.com/feed/","id":"feed/http://www.weheartswift.com/feed/","title":"We ❤ Swift","updated":1569829929574,"velocity":0.2,"subscribers":1,"website":"https://www.weheartswift.com","language":"en","description":"Swift Tutorials and iOS development"},{"feedId":"feed/https://medium.com/feed/swift-programming","id":"feed/https://medium.com/feed/swift-programming","title":"Swift Programming — Medium","updated":1554317410000,"velocity":0.0,"subscribers":1,"website":"https://medium.com/swift-programming?source=rss----5396e0e8bc29---4","language":"en","state":"dormant","description":"The Swift Programming Language - Medium"},{"feedId":"feed/http://tonyarnold.com/atom.xml","id":"feed/http://tonyarnold.com/atom.xml","title":"The blog of Tony Arnold","updated":1531312200000,"velocity":0.0,"subscribers":1,"website":"https://tonyarnold.com","language":"en","state":"dormant"},{"feedId":"feed/http://borkware.com/miniblog/rss/rss.xml","id":"feed/http://borkware.com/miniblog/rss/rss.xml","title":"Borkware Miniblog","updated":1360542004000,"velocity":0.0,"subscribers":1,"website":"https://borkwarellc.wordpress.com","language":"en","state":"dormant","description":"Bork bork bork bork."},{"feedId":"feed/http://nshipster.com/feed.xml","id":"feed/http://nshipster.com/feed.xml","title":"NSHipster","updated":1569418049005,"velocity":0.9,"subscribers":2,"website":"https://nshipster.com/","language":"en","description":"NSHipster is a journal of the overlooked bits in Objective-C, Swift, and Cocoa. Updated weekly."},{"feedId":"feed/http://feeds.feedburner.com/pilkyme","id":"feed/http://feeds.feedburner.com/pilkyme","title":"Pilky.me","updated":1561420800000,"velocity":0.1,"subscribers":1,"website":"https://pilky.me/","language":"en"},{"feedId":"feed/http://macoscope.com/blog/feed/","id":"feed/http://macoscope.com/blog/feed/","title":"[macoscope blog]","updated":1467803080000,"velocity":0.0,"subscribers":1,"website":"http://macoscope.com/blog","language":"en","state":"dormant","description":"The Macoscope Team on Designing and Developing Apps"},{"feedId":"feed/http://iosunittesting.com/feed/","id":"feed/http://iosunittesting.com/feed/","title":"iOS Unit Testing","updated":1436534796000,"velocity":0.0,"subscribers":1,"website":"http://iosunittesting.com","language":"en","state":"dormant","description":"It's about TDD, unit testing, and creating bug free code on iOS."},{"feedId":"feed/http://airspeedvelocity.net/feed/","id":"feed/http://airspeedvelocity.net/feed/","title":"Airspeed Velocity","updated":1452449244000,"velocity":0.0,"subscribers":1,"website":"https://airspeedvelocity.net","language":"en","state":"dormant","description":"African or European Swift?"},{"feedId":"feed/http://www.cimgf.com/feed/","id":"feed/http://www.cimgf.com/feed/","title":"Cocoa Is My Girlfriend","updated":1525988390000,"velocity":0.0,"subscribers":1,"website":"http://www.cimgf.com","language":"en","state":"dormant","description":"Taglines are for Windows programmers"},{"feedId":"feed/http://confusatory.org/rss","id":"feed/http://confusatory.org/rss","title":"The Confusatory","updated":1476982283000,"velocity":0.0,"subscribers":1,"website":"https://confusatory.org/","language":"en","state":"dormant","description":"cbowns’s tumblr."},{"feedId":"feed/http://indiestack.com/feed/","id":"feed/http://indiestack.com/feed/","title":"Indie Stack","updated":1569829987669,"velocity":0.2,"subscribers":1,"website":"https://indiestack.com","language":"en","description":"Hacking the Mac, iOS, and more with Daniel Jalkut"}],"label":"Programming","created":1569829874442,"enterprise":false,"numFeeds":26,"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5"},{"customizable":true,"feeds":[{"feedId":"feed/http://feedpress.me/sixcolors","id":"feed/http://feedpress.me/sixcolors","title":"Six Colors","updated":1569955568802,"velocity":24.4,"subscribers":1,"website":"https://www.sixcolors.com/","language":"en","description":"Writing about Apple and other stuff by Jason Snell, Dan Moren, and others."},{"feedId":"feed/http://corinnekrych.blogspot.com/feeds/posts/default","id":"feed/http://corinnekrych.blogspot.com/feeds/posts/default","title":"chat & code","updated":1547807580000,"velocity":0.0,"subscribers":1,"website":"http://corinnekrych.blogspot.com/","language":"en","state":"dormant","description":"Code is craft and collaboration is key to success. I love chatting the latest tech trends at coffee break: female geek."},{"feedId":"feed/http://ericasadun.com/feed/","id":"feed/http://ericasadun.com/feed/","title":"Erica Sadun","updated":1569452593000,"velocity":0.7,"subscribers":1,"website":"https://ericasadun.com","language":"en","description":"Where technology meets something or other"},{"feedId":"feed/http://therecord.co/xml/rss.xml","id":"feed/http://therecord.co/xml/rss.xml","title":"The Record","updated":1401364800000,"velocity":0.0,"subscribers":1,"website":"http://therecord.co/","language":"en","state":"dormant","description":"The stories you should know about the Mac and Cocoa developer community. Hosted by Brent Simmons and Chris Parrish."},{"feedId":"feed/https://grokswift.com/feed/index.xml","id":"feed/https://grokswift.com/feed/index.xml","title":"Grok Swift","updated":1527175834000,"velocity":0.0,"subscribers":1,"website":"https://grokswift.com/","language":"en","state":"dormant"},{"feedId":"feed/https://blog.alltheflow.com/rss/","id":"feed/https://blog.alltheflow.com/rss/","title":"All The Flow","updated":1551711655000,"velocity":0.0,"subscribers":1,"website":"http://blog.alltheflow.com/","language":"en","state":"dormant","description":"Cocoa, Swift, tools, Auto Layout - with 🧡"},{"feedId":"feed/http://onefoottsunami.com/feed/atom/","id":"feed/http://onefoottsunami.com/feed/atom/","title":"One Foot Tsunami","updated":1569938444527,"velocity":5.0,"subscribers":1,"website":"https://onefoottsunami.com","language":"en","description":"Slightly less disappointing than it sounds"},{"feedId":"feed/http://www.loopinsight.com/feed/","id":"feed/http://www.loopinsight.com/feed/","title":"Loop Insight","updated":1569953886300,"velocity":7.9,"subscribers":1,"website":"https://www.loopinsight.com","language":"en","description":"Making Sense of Technology"},{"feedId":"feed/http://beckyhansmeyer.com/feed/","id":"feed/http://beckyhansmeyer.com/feed/","title":"Becky Hansmeyer","updated":1569087367000,"velocity":1.4,"subscribers":1,"website":"https://beckyhansmeyer.com","language":"en","description":"100% grass-fed Swift"},{"feedId":"feed/http://designatednerd.com/feed/","id":"feed/http://designatednerd.com/feed/","title":"Designated Nerd","updated":1564425470000,"velocity":0.1,"subscribers":1,"website":"http://designatednerd.com","language":"en","description":"Software and Technical Support"},{"feedId":"feed/http://appcamp4girls.com/blog?format=RSS","id":"feed/http://appcamp4girls.com/blog?format=RSS","title":"Blog - App Camp For Girls","updated":1558129314000,"velocity":0.1,"subscribers":1,"website":"https://appcamp4girls.com/blog/","language":"en"},{"feedId":"feed/https://inspiredmouse.com/feed/","id":"feed/https://inspiredmouse.com/feed/","title":"Inspired Mouse","updated":1511986957000,"velocity":0.0,"subscribers":1,"website":"https://inspiredmouse.com","language":"en","state":"dormant","description":"No project is too diminutive"},{"feedId":"feed/https://incrementalistblog.wordpress.com/feed/","id":"feed/https://incrementalistblog.wordpress.com/feed/","title":"The Incrementalist.","updated":1449808490000,"velocity":0.0,"subscribers":1,"website":"https://incrementalistblog.wordpress.com","language":"en","state":"dormant","description":"notes on design"},{"feedId":"feed/http://www.virginiaroberts.com/feed/","id":"feed/http://www.virginiaroberts.com/feed/","title":"Virginia Roberts","updated":1549571065000,"velocity":0.0,"subscribers":1,"website":"http://www.virginiaroberts.com","language":"en","state":"dormant"},{"feedId":"feed/http://jessysaurusrex.com/feed/","id":"feed/http://jessysaurusrex.com/feed/","title":"jessysaurusrex","updated":1521816327000,"velocity":0.0,"subscribers":1,"website":"https://jessysaurusrex.com","language":"en","state":"dormant","description":"I wear big necklaces and (attempt to) hack things."},{"feedId":"feed/http://blog.nicoleblee.com/feed/","id":"feed/http://blog.nicoleblee.com/feed/","title":"scattered thoughts","updated":1397441482000,"velocity":0.0,"subscribers":1,"website":"http://blog.nicoleblee.com","language":"en","state":"dormant","description":"(a collection of essays and other writings)"},{"feedId":"feed/https://medium.com/feed/@tessr","id":"feed/https://medium.com/feed/@tessr","title":"Tess Rinearson on Medium","updated":1501016354000,"velocity":0.0,"subscribers":1,"website":"https://medium.com/@tessr?source=rss-c16152863954------2","language":"en","state":"dormant","description":"Stories by Tess Rinearson on Medium"},{"feedId":"feed/https://medium.com/feed/@emarley","id":"feed/https://medium.com/feed/@emarley","title":"Liz Marley on Medium","updated":1514047622000,"velocity":0.0,"subscribers":1,"website":"https://medium.com/@emarley?source=rss-b4981c59ffa5------2","language":"en","state":"dormant","description":"Stories by Liz Marley on Medium"},{"feedId":"feed/http://blog.cocoabythefire.com/rss","id":"feed/http://blog.cocoabythefire.com/rss","title":"cocoa by the fire","updated":1488945726000,"velocity":0.0,"subscribers":1,"website":"https://blog.cocoabythefire.com/","language":"en","state":"dormant","description":"Hey there, I’m Brit! Coder, Entrepreneur, Daydreamer, Lucky Wife and Mom."},{"feedId":"feed/http://www.catehuston.com/blog/feed/","id":"feed/http://www.catehuston.com/blog/feed/","title":"Accidentally in Code","updated":1567641655000,"velocity":0.2,"subscribers":1,"website":"https://cate.blog","language":"en","description":"Engineering an Interesting Life"},{"feedId":"feed/http://www.aleenmean.com/feed.xml","id":"feed/http://www.aleenmean.com/feed.xml","title":"Aleen Mean","updated":1562374146000,"velocity":0.1,"subscribers":1,"website":"https://aleenmean.com/","language":"en","description":"Technology, diversity, and miscellaneous musings by Aleen Simms."},{"feedId":"feed/http://redqueencoder.com/feed/","id":"feed/http://redqueencoder.com/feed/","title":"The Red Queen Coder","updated":1569967579535,"velocity":0.2,"subscribers":1,"website":"http://redqueencoder.com","language":"en","description":"If you give a person a program, you'll frustrate him for a day. If you teach a person to program, you will frustrate them for a lifetime!"},{"feedId":"feed/http://meaganwaller.com/index.php/feed/","id":"feed/http://meaganwaller.com/index.php/feed/","title":"Meagan Waller","updated":1403205537000,"velocity":0.0,"subscribers":1,"website":"https://meaganwaller.com","language":"en","state":"dormant"},{"feedId":"feed/http://www.myballard.com/feed/","id":"feed/http://www.myballard.com/feed/","title":"Ballard","updated":1569963964108,"velocity":8.1,"subscribers":1,"website":"https://www.myballard.com","language":"en","description":"News, events and restaurants for Seattle's Ballard and Fremont neighborhoods"},{"feedId":"feed/https://kateheddleston.com/blog/feed.atom","id":"feed/https://kateheddleston.com/blog/feed.atom","title":"KateHeddleston.com Blog Posts","updated":1526573156000,"velocity":0.0,"subscribers":1,"website":"https://www.kateheddleston.com/blog","language":"en","state":"dormant"},{"feedId":"feed/http://blog.erynwells.me/rss","id":"feed/http://blog.erynwells.me/rss","title":"Eryn Wells","updated":1442175535000,"velocity":0.0,"subscribers":1,"website":"https://blog.erynwells.me/","language":"en","state":"dormant"},{"feedId":"feed/http://lambdamaphone.blogspot.com/feeds/posts/default","id":"feed/http://lambdamaphone.blogspot.com/feeds/posts/default","title":"Everything in Context","updated":1519599000002,"velocity":0.0,"subscribers":1,"website":"http://lambdamaphone.blogspot.com/","language":"en","state":"dormant","description":"Game design, programming languages, and academia."},{"feedId":"feed/http://nothe.purplellamas.net/index.xml","id":"feed/http://nothe.purplellamas.net/index.xml","title":"Blog Posts About Stuff","updated":1512518400000,"velocity":0.0,"subscribers":1,"website":"http://nothe.purplellamas.net/","language":"en","state":"dormant","description":"Recent content on Blog Posts About Stuff"},{"feedId":"feed/http://www.mostgood.net/blog?format=RSS","id":"feed/http://www.mostgood.net/blog?format=RSS","title":"mostgood","updated":1433114183000,"velocity":0.0,"subscribers":1,"website":"http://www.mostgood.net/","language":"en","state":"dormant"},{"feedId":"feed/http://www.mistys-internet.website/blog/atom.xml","id":"feed/http://www.mistys-internet.website/blog/atom.xml","title":"The Future Is Now","updated":1550722972000,"velocity":0.0,"subscribers":1,"website":"http://mistys-internet.website/blog/","language":"en","state":"dormant"},{"feedId":"feed/http://pewpewthespells.com/feed.xml","id":"feed/http://pewpewthespells.com/feed.xml","title":"Samantha Marshall's Blog","updated":1569829870614,"velocity":8.6,"subscribers":1,"website":"https://pewpewthespells.com/","language":"en","description":"Blog Feed"},{"feedId":"feed/http://blog.ashleynh.me/rss/","id":"feed/http://blog.ashleynh.me/rss/","title":"Ashley Nelson-Hornstein","updated":1496606482000,"velocity":0.0,"subscribers":1,"website":"http://ashleynh.me:80/","language":"en","state":"dormant","description":"Ashley Nelson"},{"feedId":"feed/https://medium.com/feed/@nerdonica","id":"feed/https://medium.com/feed/@nerdonica","title":"Veronica Ray on Medium","updated":1471184706000,"velocity":0.0,"subscribers":1,"website":"https://medium.com/@nerdonica?source=rss-eaf18ccd367f------2","language":"en","state":"dormant","description":"Stories by Veronica Ray on Medium"},{"feedId":"feed/http://www.nadynerichmond.com/blog/feed/","id":"feed/http://www.nadynerichmond.com/blog/feed/","title":"go ahead, mac my day","updated":1506105463000,"velocity":0.0,"subscribers":1,"website":"http://www.nadynerichmond.com/blog","language":"en","state":"dormant","description":"a Macintosh girl in a Microsoft world"},{"feedId":"feed/http://www.bbc.co.uk/blogs/doctorwho/rss","id":"feed/http://www.bbc.co.uk/blogs/doctorwho/rss","title":"Doctor Who","updated":1567440000000,"velocity":0.2,"subscribers":1,"website":"https://www.bbc.co.uk/blogs/doctorwho","language":"en","description":"All the latest news and features from the world of Doctor Who."},{"feedId":"feed/http://scripting.com/rss.xml","id":"feed/http://scripting.com/rss.xml","title":"Scripting News","updated":1569956453445,"velocity":15.4,"subscribers":1,"website":"http://scripting.com/","language":"en","description":"Scripting News, the weblog started in 1994 that bootstrapped the blogging revolution. 🚀"},{"feedId":"feed/http://daringfireball.net/feeds/main","id":"feed/http://daringfireball.net/feeds/main","title":"Daring Fireball","updated":1569960343721,"velocity":13.1,"subscribers":1,"website":"https://daringfireball.net/","language":"en","description":"By John Gruber"},{"feedId":"feed/http://www.mechanicalgirl.com/feeds/all/","id":"feed/http://www.mechanicalgirl.com/feeds/all/","title":"MechanicalGirl","updated":1569829801167,"velocity":1.1,"subscribers":1,"website":"http://www.MechanicalGirl.com/","language":"en","description":"Latest posts on MechanicalGirl"},{"feedId":"feed/http://ranchero.com/xml/rss.xml","id":"feed/http://ranchero.com/xml/rss.xml","title":"ranchero.com","updated":1569971250209,"velocity":2.3,"subscribers":1,"website":"https://inessential.com/","language":"en","description":"Brent Simmons’s weblog."},{"feedId":"feed/http://natashatherobot.com/feed/","id":"feed/http://natashatherobot.com/feed/","title":"Natasha The Robot","updated":1569829929101,"velocity":0.2,"subscribers":2,"website":"https://www.natashatherobot.com","language":"en"},{"feedId":"feed/http://daringfireball.net/index.xml","id":"feed/http://daringfireball.net/index.xml","title":"Daring Fireball","updated":1569959070397,"velocity":15.6,"subscribers":6,"website":"https://daringfireball.net/","language":"en","description":"By John Gruber"},{"feedId":"feed/http://timekl.com/atom.xml","id":"feed/http://timekl.com/atom.xml","title":"don't panic","updated":1555225200000,"velocity":0.0,"subscribers":1,"website":"https://timekl.com/","language":"en","description":"Occasional posts, usually about technology"},{"feedId":"feed/http://nataliepo.typepad.com/nataliepo/rss.xml","id":"feed/http://nataliepo.typepad.com/nataliepo/rss.xml","title":"nataliepo (posts on 'nataliepo' (rss 2.0))","updated":1447256454000,"velocity":0.0,"subscribers":1,"website":"https://nataliepo.typepad.com/nataliepo/","language":"en","state":"dormant"},{"feedId":"feed/http://shapeof.com/rss.xml","id":"feed/http://shapeof.com/rss.xml","title":"The Shape of Everything","updated":1569263479000,"velocity":1.6,"subscribers":1,"website":"https://shapeof.com/","language":"en","description":"A website mostly about Mac stuff, written by Gus Mueller"},{"feedId":"feed/http://jvns.ca/atom.xml","id":"feed/http://jvns.ca/atom.xml","title":"Julia Evans","updated":1569959500644,"velocity":4.3,"subscribers":2,"website":"http://jvns.ca","language":"en"},{"feedId":"feed/https://www.natashatherobot.com/feed/","id":"feed/https://www.natashatherobot.com/feed/","title":"Natasha the Robot","updated":1545498499000,"velocity":0.0,"subscribers":1,"website":"https://www.natashatherobot.com","language":"en","state":"dormant"},{"feedId":"feed/http://pointersgonewild.com/feed/","id":"feed/http://pointersgonewild.com/feed/","title":"Pointers Gone Wild","updated":1560168135000,"velocity":0.1,"subscribers":1,"website":"https://pointersgonewild.com","language":"en","description":"A blog about compilers, programming and technology."},{"feedId":"feed/http://www.kristinathai.com/feed/","id":"feed/http://www.kristinathai.com/feed/","title":"kristinathai.com","updated":1563831802000,"velocity":0.1,"subscribers":1,"website":"http://www.kristinathai.com","language":"ja"},{"feedId":"feed/https://developer.apple.com/swift/blog/news.rss","id":"feed/https://developer.apple.com/swift/blog/news.rss","title":"Swift Blog - Apple Developer","updated":1476306000000,"velocity":0.0,"subscribers":1,"website":"https://developer.apple.com/swift/blog/","language":"en","state":"dormant","description":"Get the latest news and helpful tips on the Swift programming language from the engineers who created it."},{"feedId":"feed/http://www.rebeccamiller-webster.com/feed/","id":"feed/http://www.rebeccamiller-webster.com/feed/","title":"Rebecca Miller-Webster","updated":1547836736000,"velocity":0.0,"subscribers":1,"website":"https://www.rebeccamiller-webster.com","language":"en","state":"dormant","description":"Ruby + JavaScript"},{"feedId":"feed/http://swift.ayaka.me/posts?format=RSS","id":"feed/http://swift.ayaka.me/posts?format=RSS","title":"Learn Swift ↯","updated":1466314290000,"velocity":0.0,"subscribers":1,"website":"http://swift.ayaka.me/","language":"en","state":"dormant"},{"feedId":"feed/http://www.imore.com/rss.xml","id":"feed/http://www.imore.com/rss.xml","title":"iMore","updated":1569969502355,"velocity":17.8,"subscribers":1,"website":"https://www.imore.com/","language":"en","description":"More news and rumors, more help and how-tos, more app and accessory reviews, more iPhone and iPad and iPod touch. More of everything you love. iMore."},{"feedId":"feed/http://blog.thoughtbrain.com/feed/","id":"feed/http://blog.thoughtbrain.com/feed/","title":"Feed: Thoughtbrain Bloggers","updated":1426140251000,"velocity":0.0,"subscribers":1,"website":"http://blog.thoughtbrain.com","language":"en","state":"dormant","description":"Designery nerdy things."},{"feedId":"feed/http://blog.ellenchisa.com/feed/","id":"feed/http://blog.ellenchisa.com/feed/","title":"Ellen's Blog","updated":1546640740000,"velocity":0.0,"subscribers":1,"website":"https://blog.ellenchisa.com?source=rss----da542b929da2---4","language":"en","state":"dormant","description":"I’m starting a new company with @paulbiggar, which you can learn about at https://darklang.com. I mostly write about startups and software development. - Medium"},{"feedId":"feed/https://medium.com/feed/@jaimeejaimee","id":"feed/https://medium.com/feed/@jaimeejaimee","title":"jaimeejaimee","updated":1558376026000,"velocity":0.1,"subscribers":1,"website":"https://medium.com/@jaimeejaimee?source=rss-11d5cc4494a2------2","language":"en","description":"Stories by jaimeejaimee on Medium"}],"label":"Uncategorized","created":1569829699432,"enterprise":false,"numFeeds":55,"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8"},{"customizable":true,"feeds":[{"feedId":"feed/http://bryan.io/rss","id":"feed/http://bryan.io/rss","title":"bryan i/o","updated":1567230243000,"velocity":0.2,"subscribers":1,"website":"https://bryan.io/","language":"en","description":"Software engineer who led iOS at Tumblr from 2012-2015. Mostly cheeseburgers at this point. Over at irace.me nowadays."},{"feedId":"feed/http://nickbradbury.com/feed/","id":"feed/http://nickbradbury.com/feed/","title":"Nick Bradbury","updated":1503946502000,"velocity":0.0,"subscribers":1,"website":"https://nickbradbury.com","language":"en","state":"dormant","description":"I develop Android apps. In a previous life I created HomeSite, TopStyle and FeedDemon for Windows."},{"feedId":"feed/http://feeds.feedburner.com/domainofthebored","id":"feed/http://feeds.feedburner.com/domainofthebored","title":"Peter Hosey","updated":1567916255000,"velocity":0.2,"subscribers":1,"website":"https://boredzo.org/blog","language":"en","description":"The personal weblog of Peter Hosey."},{"feedId":"feed/http://blog.metaobject.com/feeds/posts/default","id":"feed/http://blog.metaobject.com/feeds/posts/default","title":"metablog","updated":1556396880001,"velocity":0.0,"subscribers":1,"website":"https://blog.metaobject.com/","language":"en"},{"feedId":"feed/http://feeds2.feedburner.com/adobe/jnack","id":"feed/http://feeds2.feedburner.com/adobe/jnack","title":"John Nack on Adobe (rss (feedburner))","updated":1391820146000,"velocity":0.0,"subscribers":1,"website":"http://blogs.adobe.com/jnack","language":"en","state":"dormant"},{"feedId":"feed/http://www.zathras.de/angelweb/BlogRSSFeed.rss","id":"feed/http://www.zathras.de/angelweb/BlogRSSFeed.rss","title":"Zathras.de - Uli's most useless blog in the World","updated":1562364000000,"velocity":0.1,"subscribers":1,"website":"https://orangejuiceliberationfront.com/","language":"en","description":"Uli's blog on programming, game development, pop culture and other boring things."},{"feedId":"feed/http://typesetinthefuture.com/feed/","id":"feed/http://typesetinthefuture.com/feed/","title":"Typeset In The Future","updated":1544536375000,"velocity":0.0,"subscribers":1,"website":"https://typesetinthefuture.com","language":"en","state":"dormant","description":"Typography and Design in Science Fiction Movies"},{"feedId":"feed/http://david-smith.org/atom.xml","id":"feed/http://david-smith.org/atom.xml","title":"David Smith","updated":1569830048565,"velocity":0.5,"subscribers":1,"website":"http://david-smith.org/","language":"en"},{"feedId":"feed/http://awkwardhare.com/rss","id":"feed/http://awkwardhare.com/rss","title":"Awkward Hare","updated":1484149569000,"velocity":0.0,"subscribers":1,"website":"https://awkwardhare.com/","language":"en","state":"dormant","description":"A blog by Greg Pierce"},{"feedId":"feed/http://frozendevil.com/atom.xml","id":"feed/http://frozendevil.com/atom.xml","title":"frozendevil","updated":1398927600000,"velocity":0.0,"subscribers":1,"website":"http://frozendevil.com/","language":"en","state":"dormant"},{"feedId":"feed/http://useyourloaf.com/blog/rss.xml","id":"feed/http://useyourloaf.com/blog/rss.xml","title":"Use Your Loaf","updated":1569830765949,"velocity":0.5,"subscribers":2,"website":"https://useyourloaf.com/blog/","language":"en","description":"Recent content on Use Your Loaf - iOS Development News & Tips"},{"feedId":"feed/http://www.appleoutsider.com/feed/","id":"feed/http://www.appleoutsider.com/feed/","title":"Apple Outsider","updated":1402413759000,"velocity":0.0,"subscribers":1,"website":"https://www.appleoutsider.com","language":"en","state":"dormant"},{"feedId":"feed/http://rathole.tumblr.com/rss","id":"feed/http://rathole.tumblr.com/rss","title":"RatHole","updated":1554476924000,"velocity":0.0,"subscribers":1,"website":"https://rathole.tumblr.com/","language":"en","description":"what my brain does when I’m not looking"},{"feedId":"feed/http://codeplease.io/rss/","id":"feed/http://codeplease.io/rss/","title":"Codeplease","updated":1511101649000,"velocity":0.2,"subscribers":1,"website":"http://codeplease.io/","language":"en","state":"dead","description":"Ramblings about code"},{"feedId":"feed/http://blog.jaredsinclair.com/rss?1","id":"feed/http://blog.jaredsinclair.com/rss?1","title":"Jared Sinclair","updated":1554679244000,"velocity":0.0,"subscribers":1,"website":"https://jaredsinclair.com/","language":"en","description":"Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description."},{"feedId":"feed/http://www.takingnotes.co/atom.xml","id":"feed/http://www.takingnotes.co/atom.xml","title":"Doug Russell","updated":1541721600000,"velocity":0.0,"subscribers":1,"website":"http://takingnotes.co//","language":"en","state":"dormant"},{"feedId":"feed/http://www.red-sweater.com/blog/feed","id":"feed/http://www.red-sweater.com/blog/feed","title":"Red Sweater","updated":1568998271000,"velocity":0.5,"subscribers":1,"website":"https://red-sweater.com/blog","language":"en","description":"Official blog of Red Sweater Software"},{"feedId":"feed/http://mjtsai.com/blog/feed/","id":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","updated":1569964972836,"velocity":18.7,"subscribers":1,"website":"https://mjtsai.com/blog","language":"en"},{"feedId":"feed/http://jnack.com/blog/?feed=rss2","id":"feed/http://jnack.com/blog/?feed=rss2","title":"Nackblog","updated":1569939043384,"velocity":2.7,"subscribers":1,"website":"http://jnack.com/blog","language":"en","description":"Musings on photography, illustration, mobile apps, and more"},{"feedId":"feed/http://bitsplitting.org/feed/","id":"feed/http://bitsplitting.org/feed/","title":"Daniel Jalkut","updated":1563906209098,"velocity":0.1,"subscribers":1,"website":"https://bitsplitting.org","language":"en","description":"Chasing the impossible with Daniel Jalkut"},{"feedId":"feed/http://stmts.net/feed/","id":"feed/http://stmts.net/feed/","title":"Jesper","updated":1298233430000,"velocity":0.0,"subscribers":1,"website":"http://stmts.net","language":"en","state":"dormant","description":"On programming"},{"feedId":"feed/http://www.mikeash.com/pyblog/rss.py","id":"feed/http://www.mikeash.com/pyblog/rss.py","title":"NSBlog","updated":1530280470876,"velocity":0.0,"subscribers":2,"website":"http://www.mikeash.com/pyblog/","language":"en","state":"dormant","description":"Mac OS X and Cocoa programming"},{"feedId":"feed/http://corporationunknown.com/blog/feed/","id":"feed/http://corporationunknown.com/blog/feed/","title":"Corporation Unknown","updated":1420427842000,"velocity":0.0,"subscribers":1,"website":"http://corporationunknown.com/blog","language":"en","state":"dormant"},{"feedId":"feed/http://jamesdempsey.net/feed/","id":"feed/http://jamesdempsey.net/feed/","title":"James Dempsey","updated":1568304414000,"velocity":0.2,"subscribers":1,"website":"https://jamesdempsey.net","language":"en","description":"From Apple to Indie in three easy steps"},{"feedId":"feed/http://brian-webster.tumblr.com/rss","id":"feed/http://brian-webster.tumblr.com/rss","title":"Very Web. Such Blog. Wow.","updated":1530051926000,"velocity":0.0,"subscribers":1,"website":"https://brian-webster.tumblr.com/","language":"en","state":"dormant","description":"Brian Webster’s sporadic blogging about mostly programming stuff."},{"feedId":"feed/http://dangillmor.com/feed/","id":"feed/http://dangillmor.com/feed/","title":"Dan Gillmor","updated":1565895640000,"velocity":0.1,"subscribers":1,"website":"http://dangillmor.com","language":"en","description":"Just in case you were still wondering…"},{"feedId":"feed/http://www.jeffmcleman.com/blog/feed/","id":"feed/http://www.jeffmcleman.com/blog/feed/","title":"Jeff McLeman","updated":1563565996000,"velocity":0.1,"subscribers":1,"website":"https://www.jeffmcleman.com/blog","language":"en","description":"The Brooding Thoughts of an Untamed Mind"},{"feedId":"feed/http://www.caseyliss.com/rss","id":"feed/http://www.caseyliss.com/rss","title":"Liss is More","updated":1569830046093,"velocity":0.5,"subscribers":1,"website":"https://www.caseyliss.com","language":"en","description":"Posts to Liss is More"},{"feedId":"feed/http://ignorethecode.net/blog/rss/","id":"feed/http://ignorethecode.net/blog/rss/","title":"ignorethecode.net","updated":1532170394000,"velocity":0.0,"subscribers":1,"website":"http://ignorethecode.net","language":"en","state":"dormant","description":"Essays on usability, programming, and other nerd topics."},{"feedId":"feed/http://sheilasweblog.wordpress.com/feed/","id":"feed/http://sheilasweblog.wordpress.com/feed/","title":"Sheila's Weblog","updated":1237602766000,"velocity":0.0,"subscribers":1,"website":"https://sheilasweblog.wordpress.com","language":"en","state":"dormant","description":"Quilting, kitties, other fun stuff."},{"feedId":"feed/http://www.allenpike.com/feed/","id":"feed/http://www.allenpike.com/feed/","title":"Allen Pike","updated":1569920023707,"velocity":0.5,"subscribers":1,"website":"https://www.allenpike.com/","language":"en"},{"feedId":"feed/https://developer.apple.com/news/rss/news.rss","id":"feed/https://developer.apple.com/news/rss/news.rss","title":"iPhone Developer News","updated":1569869696571,"velocity":3.2,"subscribers":1,"website":"https://developer.apple.com/news/","language":"en","description":"Apple Developer News and Updates feed provided by Apple, Inc."},{"feedId":"feed/http://themainthread.com/feed.xml","id":"feed/http://themainthread.com/feed.xml","title":"The Main Thread","updated":1440820800000,"velocity":0.0,"subscribers":1,"website":"http://themainthread.com/","language":"en","state":"dormant"},{"feedId":"feed/http://www.gordonmeyer.com/atom.xml","id":"feed/http://www.gordonmeyer.com/atom.xml","title":"Gordon Meyer (posts on 'gordon meyer' (atom))","updated":1569066494000,"velocity":0.5,"subscribers":1,"website":"https://www.gordonmeyer.com/","language":"en"},{"feedId":"feed/http://www.mondaynote.com/feed/","id":"feed/http://www.mondaynote.com/feed/","title":"Monday Note","updated":1569786821000,"velocity":2.0,"subscribers":1,"website":"https://mondaynote.com?source=rss----c537d80ed0a---4","language":"en","description":"Media, Tech, Business Models viewed from Palo Alto and Paris - Medium"},{"feedId":"feed/http://www.randsinrepose.com/index.xml","id":"feed/http://www.randsinrepose.com/index.xml","title":"Rands In Repose","updated":1569830107339,"velocity":0.2,"subscribers":4,"website":"https://randsinrepose.com","language":"en"},{"feedId":"feed/http://furbo.org/feed/","id":"feed/http://furbo.org/feed/","title":"furbo.org","updated":1569830050733,"velocity":0.5,"subscribers":1,"website":"https://furbo.org","language":"en","description":"by Craig Hockenberry"},{"feedId":"feed/http://feeds.feedburner.com/NSHipster","id":"feed/http://feeds.feedburner.com/NSHipster","title":"NSHipster","updated":1569222000000,"velocity":0.7,"subscribers":1,"website":"https://nshipster.com/","language":"en","description":"NSHipster is a journal of the overlooked bits in Objective-C, Swift, and Cocoa. Updated weekly."},{"feedId":"feed/http://www.neglectedpotential.com/feed/","id":"feed/http://www.neglectedpotential.com/feed/","title":"Neglected Potential","updated":1537811051000,"velocity":0.0,"subscribers":1,"website":"http://www.neglectedpotential.com","language":"en","state":"dormant"}],"label":"Weblogs","created":1569829952574,"enterprise":false,"numFeeds":39,"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366"}] \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/AddFeed/mustread_addfeed.json b/Frameworks/Account/AccountTests/Feedly/AddFeed/mustread_addfeed.json deleted file mode 100644 index b8a5afd50..000000000 --- a/Frameworks/Account/AccountTests/Feedly/AddFeed/mustread_addfeed.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","updated":1569971230061,"continuation":"16d81261cbd:4d986:18991ffa","items":[{"originId":"https://inessential.com/2019/10/01/on_bullying_in_our_community","fingerprint":"446362f9","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8993d56d:5148e:18991ffa","summary":{"direction":"ltr","content":"Janie Larson writes about being bullied, and you should read it.
\nIt’s natural to wonder who the bully is and who the conference organizers are — but I’m resisting the temptation to spend any time on it. It’s not a puzzle to be solved. Janie’s explicit that she doesn’t want this to result in anyone getting harassed, and she doesn’t want to start a feud. Respect that.
\nInstead, she talks about the human cost of being bullied, and she presents a guide for handling bullying — which is written especially for people witnessing it.
\nEven if you think it’s unlikely that you yourself will ever be bullied (and you might not think that), it’s worth remembering that you might see it happen to someone else. I hope you and I would do the right thing.
"},"alternate":[{"href":"https://inessential.com/2019/10/01/on_bullying_in_our_community","type":"text/html"}],"crawled":1569971230061,"title":"On Bullying in Our Community","published":1569969481000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"visual":{"url":"http://cdn3.sbnation.com/entry_photo_images/9192995/Untitled_large.jpg","width":630,"height":420,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/10/01/nanowrimo_is_next_month_and_i_will_conti","fingerprint":"8549ffd6","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa","summary":{"direction":"ltr","content":"NaNoWriMo is next month, and I will continue my streak of not participating in it. I’m super-impressed by the people who do, though.
\nIt would take me a month of hard, solid work all November to decide on an idea to write about, then another month to think it through some more — or two months, really, because the holidays get in the way — and then about a year of nightly work to decide on a plot outline and characters and tone, and then another year of refining that outline, and then, by NaNoWriMo 2021 or 2022, I’d be ready to start writing. I suspect I’d average about 300 words a day, which would get me about 9,000 words for the month — which is well less than a novel or even the 50,000 words goal.
\nI blog instead.
\nPS What made me think of this: Cheri Baker, Let’s Half-Ass NaNoWriMo Together.
"},"alternate":[{"href":"https://inessential.com/2019/10/01/nanowrimo_is_next_month_and_i_will_conti","type":"text/html"}],"crawled":1569963964265,"published":1569960276000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/galaxygear-lead.jpg","width":619,"height":411,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/10/01/this_leaked_audio_from_facebook_https_ww","fingerprint":"abd6439f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa","summary":{"direction":"ltr","content":"This leaked audio from Facebook — where Mark Zuckerberg promises that “you go to the mat and you fight” Elizabeth Warren — is a reminder: any corporation that has the power over the speech of billions of people is still a corporation with its own interests. And those interests don’t match yours or mine or the interests of democracy.
\nYou don’t have to support, or even like, Elizabeth Warren to understand that.
\nDo you trust Facebook not to tip the scales in favor of Zuckerberg’s interests? I sure don’t.
\nThis is about Facebook and a specific presidential candidate — and it’s also about giant corporate communications platforms and how they subvert civilization.
\nPS Reminder: Instagram is Facebook too.
"},"alternate":[{"href":"https://inessential.com/2019/10/01/this_leaked_audio_from_facebook_https_ww","type":"text/html"}],"crawled":1569949378209,"published":1569946773000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/25/i_had_the_fun_of_interviewing_old_friend","fingerprint":"efab5851","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d996:18991ffa","summary":{"direction":"ltr","content":"I had the fun of interviewing old friend Daniel Jalkut on the latest episode of The Omni Show.
"},"alternate":[{"href":"https://inessential.com/2019/09/25/i_had_the_fun_of_interviewing_old_friend","type":"text/html"}],"crawled":1569829821629,"published":1569437386000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/13/netnewswire_5_0_1_released","fingerprint":"f53acc86","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d995:18991ffa","summary":{"direction":"ltr","content":"\nNetNewsWire 5.0.1 is almost entirely a bug-fix release — see the release notes for the full scoop.
\nIt includes one sort-of new feature: there’s now a checkbox in Preferences for turning off the unread count in the Dock. (It was a hidden pref — now it’s visible.)
\nStatus
\nHere’s what else we’re working on:
\n- \n
- iOS/iPadOS app \n
- NetNewsWire 5.0.2 for Mac — which will mainly be about performance (yes, we can make it even faster) \n
- NetNewsWire 5.1 for Mac — tentative feature list includes content extraction and at least one more syncing option (but we might change our minds on these: anything can happen between now and then) \n
We might also distribute NetNewsWire 5.0.2 for Mac on the Mac App Store. No guarantees yet, of course, but work is happening in that direction. This goes to our goal of getting as many people as possible using RSS readers.
"},"alternate":[{"href":"https://inessential.com/2019/09/13/netnewswire_5_0_1_released","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5.0.1 Released","published":1568408217000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/10/had_to_get_a_new_key_fob_at_work_today_m","fingerprint":"5b6c292f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d994:18991ffa","summary":{"direction":"ltr","content":"Had to get a new key fob at work today — my old one wore out. Just a couple weeks shy of my fifth anniversary at Omni! Time flies.
\nI figure I’m just over eight years from retiring, so I’m not even halfway done here. :)
"},"alternate":[{"href":"https://inessential.com/2019/09/10/had_to_get_a_new_key_fob_at_work_today_m","type":"text/html"}],"crawled":1569829821629,"published":1568153137000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/06/on_syncing_netnewswire_using_icloud","fingerprint":"3b5ade1b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d993:18991ffa","summary":{"direction":"ltr","content":"People have been asking me about supporting iCloud as a sync method for NetNewsWire.
\nIt would be really cool because:
\n- \n
- There’s no sign-in \n
- It’s free — no need to spend money on another service \n
- It would help broaden the pool of people using RSS, since there would be no additional expense or service they’d need — they could just get going \n
It’s a great idea — no question. Given that my goal is to get as many people as possible using RSS, this makes total sense.
\nWhy we didn’t ship with this feature
\nFor the first release — I still think of it as a 1.0, because it really is — our best bet was to appeal to people already using an existing RSS service. We know that those people like and use RSS, and they’re the people most likely to check out a new RSS app.
\n(We could have delayed and shipped with support for more existing services, but we figured one was enough to get started with, and we could add other services later. And we are.)
\nIn other words, we tried to make an app that the existing market would like. And that’s the right call when you’re starting out.
\nAlso: iCloud sync makes the most sense when you have both a Mac and an iOS app, and we don’t — the iOS app is still in progress. We totally expect people to use NetNewsWire on the Mac and Unread or Reeder on their iPhone and iPad — and iCloud sync won’t work across apps. This scenario requires using services such as Feedbin.
\nWhy I have no idea when this feature might appear
\nFor any existing RSS service, we can be confident that our effort to support it in NetNewsWire would be successful. This is well-trodden ground: we make some web API calls, integrate with our database, and done. It’s not nothing, but conceptually it’s simple and there’s no cause to worry about technical issues.
\nBut iCloud syncing will mean writing exploratory code and only then finding out if it’s going to work.
\nSyncing the feeds list should be relatively easy — the real issue is with syncing read/unread/starred states of articles. That means a lot of small records.
\nIs CloudKit up to this? What are the limits? How fast is it? How reliable?
\nWe just don’t know.
\nYes, it’s encouraging that News Explorer has this feature — but that doesn’t tell us much about the limits, reliability, and performance.
\nWorking on this is a risk.
\nSo — as you can imagine — we’re still more keen on supporting existing RSS services, because we know there are plenty of people who for-sure like RSS, and who might like NetNewsWire, but who won’t switch their syncing system just to use NetNewsWire.
\nThat said: I do think we’ll get around to trying this, and I’ll be super-pleased if it works, because it really is a great idea — but we have a bunch of other work to do first. (Including the iOS app!)
"},"alternate":[{"href":"https://inessential.com/2019/09/06/on_syncing_netnewswire_using_icloud","type":"text/html"}],"crawled":1569829821629,"title":"On Syncing NetNewsWire Using iCloud","published":1567817061000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/06/_markos_charatzas_writes_https_qnoid_com","fingerprint":"ca200f5a","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d992:18991ffa","summary":{"direction":"ltr","content":"Markos Charatzas writes about his excitement in joining the Apple developer world in 2009 to his eventual disillusionment today.
"},"alternate":[{"href":"https://inessential.com/2019/09/06/_markos_charatzas_writes_https_qnoid_com","type":"text/html"}],"crawled":1569829821629,"published":1567788970000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/04/on_the_many_netnewswire_feature_requests","fingerprint":"66196df9","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d991:18991ffa","summary":{"direction":"ltr","content":"A number of people have asked that NetNewsWire show the full web page — right there, in the app — after clicking a link.
\nThe idea is pretty good! It solves two big problems:
\n- \n
- You get full content, which is great when a feed contains only summaries or truncated articles \n
- You don’t have to switch to another app: you can stay right where you are \n
You’d think it’s a no-brainer, and we should just go ahead. But there are other considerations.
\nOne big one is that your ad blockers and privacy extensions won’t run. They work in Safari, but they do not extend to other apps that use WebKit. This means that viewing a web page in NetNewsWire would be less secure and more annoying than viewing the same page in Safari (or whatever your browser is).
\nThis points to one of my design principles: the app should have boundaries. Some features belong in the app, and some features are best left to apps that do that feature way better than NetNewsWire could. One of those things is showing web pages — that’s really a web browser feature.
\nHaving boundaries means we can concentrate on doing a great job at the things that do belong in the app.
\n(Before you mention SFSafariViewController, recall that it’s iOS-only.)
\nWhat about the glory days?
\n“But Brent! In NetNewsWire 2.0 you added a tabbed browser to NetNewsWire, and it was awesome and a hugely popular feature!”
\nIt was! But times have changed. Many websites are hostile these days. In 2005, this feature was fine — but these days it’s totally not.
\nA winged messenger arrives with a solution
\nThere is a solution to the problem of showing full content and not leaving the app, and it’s a feature that really does belong in an RSS reader: using content extraction to grab the article from the original page.
\nIf you’ve ever used Safari’s Reader view, then you know what I’m talking about. The idea is that NetNewsWire would do something very much like the Reader view (but inline, in the article pane), that grabs the content and formats it nicely, without all the extra junk that is not the article you want to read.
\nThere are a number of open source options for this. We’re looking at using Feedbin’s content extraction service (which wouldn’t require you to have a Feedbin account).
\nThe generous folks at Feedbin are running a copy of the open-source Mercury Parser, and they’ve offered to open this service up to RSS readers like NetNewsWire. (Reeder uses it already, for instance.)
\nWhen?
\nRight now we’re working on NetNewsWire 5.0.1, which is (almost entirely) a bug-fix release. I don’t know what’s going to be in 5.1 yet — we’re still digesting all the feedback, looking at our original roadmap, and thinking about things.
\nWe’re also working on NetNewsWire for iOS! We’re busy.
\nBut this is definitely the kind of feature that should come sooner rather than later.
"},"alternate":[{"href":"https://inessential.com/2019/09/04/on_the_many_netnewswire_feature_requests","type":"text/html"}],"crawled":1569829821629,"title":"On the Many NetNewsWire Feature Requests to Show Full Web Pages","published":1567661107000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/09/02/on_my_funny_ideas_about_what_beta_means","fingerprint":"bb260103","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d990:18991ffa","summary":{"direction":"ltr","content":"John Gruber has mentioned, on The Talk Show, that I’ve got some weird ideas about what beta means.
\nHere are my definitions:
\ndevelopment (d): everything is in progress and the app might be completely unusable.
\nalpha (a): the app is feature-complete and has no known bugs — but, importantly, it’s had very little testing.
\nbeta (b): the app is feature-complete, has no known bugs, and has been tested — but further testing is still warranted. Every beta is a release candidate.
\nThese are defined in a NetNewsWire Technote. It’s important to have definitions that everybody working on or testing the app understands.
\nBut why these rather strict definitions?
\nIt’s part of our commitment to quality. What matters is the end result — the shipping app — and these definitions make sure we don’t get to beta, or even alpha, with the app up on the table with wires sticking out and pieces missing.
\nThis gives us a big space between development and shipping, and that space is all about making sure the bugs are all fixed.
\nThis is a matter of ethics and pride in our work. Absolutely.
\nBut it’s also pragmatic. This is an open source app, written by volunteers in their spare time, and having this rhythm baked-in to the process helps make sure we can uphold our standards even without full-time developers, managers, and testers.
\n* * *
\nAnd… it bugs me how little real attention our industry pays to quality these days. In some cases the consequences are disastrous; in other cases they’re merely expensive. It doesn’t have to be this way.
\nIf it seems like I’m going too far with my definitions, well, I’m trying to bend the stick here.
"},"alternate":[{"href":"https://inessential.com/2019/09/02/on_my_funny_ideas_about_what_beta_means","type":"text/html"}],"crawled":1569829821629,"title":"On My Funny Ideas About What Beta Means","published":1567455823000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/31/i_love_this_netnewswire_write_up_on_wp_t","fingerprint":"e526eb19","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98f:18991ffa","summary":{"direction":"ltr","content":"I love this NetNewsWire write-up on WP Tavern.
"},"alternate":[{"href":"https://inessential.com/2019/08/31/i_love_this_netnewswire_write_up_on_wp_t","type":"text/html"}],"crawled":1569829821629,"published":1567280353000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/31/netnewswire_5_feature_requests","fingerprint":"57ea983b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98e:18991ffa","summary":{"direction":"ltr","content":"NetNewsWire 5.0 is a 1.0 app in disguise.
\nAnd so, as expected, we’ve had a ton of feature requests. Most people tend to request one or two features — and there’s a huge variety in these. People want different things.
\nNevertheless, there are a few themes we can pick out from what people are asking for:
\n- \n
- More syncing options, especially Feedly support \n
- iOS app \n
- Some way to deal with partial-content feeds \n
- Customization of the article pane (fonts, colors, etc.) \n
- Traditional view (timeline on top with single lines, article below) \n
- More sharing options (Instapaper, Pinboard, etc.) \n
- Customizable keyboard shortcuts \n
- State restoration \n
- Localizations \n
- Hiding read items in the timeline (or dimming them) \n
- Hiding feeds (in the sidebar) that have no unread articles \n
- User-created smart feeds \n
The less-common, more singular requests are for things like specific sorting options — there are lots of different small options that people would like.
\nPeople have also asked for things that might surprise you (they surprised me) — for instance, we’ve had a request for monochrome icons for the toolbar. Another request for a Dark Mode that’s different from Apple’s Dark Mode. Etc.
\nHow We Choose What To Do Next
\nThe first principle is that we can’t lose what we love about the app. We do our damnedest to ship with no bugs, and the app needs to be fast and, most importantly, it needs to feel lighter-than-air.
\nWhenever you add things — even if the app remains just as fast, even if there are no bugs — you still run the risk of losing that feeling of lightness. One of the quickest ways to lose that feeling is to add a whole bunch of preferences, View menu options, toolbar commands, and other chrome. So we’re going to be very slow to add things like that.
\nNetNewsWire needs to not become fiddly. (Earlier versions of NetNewsWire got way too fiddly.)
\nThere are other questions we ask about a feature before we do it.
\n- \n
- Will it substantially benefit current users? \n
- Will it bring a number of new users to the app? \n
- Does the feature depend on something else being done first? \n
- How much work will it take? \n
- Does it require resources (such as new icons) that our programmers can’t provide? \n
- Does the feature really belong in an RSS reader at all? \n
And, because this is an open source app, there’s another dimension: people. Is someone available? Has someone just shown up who’s eager to work on a specific feature? Those things have an impact on scheduling, too.
\nThe good news is that most of the common feature requests are obvious things to do.
\nSome examples — not nearly everything, just a few thoughts:
\nThe iOS app is in progress. Maurice Parker has been writing it, and it’s coming along very well. Still plenty more to do, and we won’t ship before iOS 13 ships, but it’s happening.
\nAdding syncing options is a definite good thing for the app. Doing the first one (Feedbin) was the big effort, because it required building the infrastructure that makes syncing possible. Once that was done, adding additional services is not super-difficult. (Not easy, no. Nothing’s trivial. But at least the infrastructure and patterns are in place.)
\nWe’d like to support all the various services, or at least a majority of them. And we have people working on adding services.
\nCustomization of the article pane will most likely work the way it did in older versions of NetNewsWire: we had theme files which included templates and CSS. The app shipped with a few, and you could make your own and use themes other people made.
\nThis feature shipped with NetNewsWire 2.0, and people really loved it. It was fun!
\nMore sharing options is an obvious good idea. Of course you should be able to send to Instapaper, Pocket, Pinboard, and so on. We shipped with custom support for MarsEdit and the Micro.blog app — mainly because I use those apps. But an RSS reader ought to support as many sharing workflows as possible. That’s one of the core points of the app.
\n* * *
\nAnyway — the above doesn’t cover everything. Don’t take any of the above as gospel about what we’re doing or when, or what we’re not doing. We haven’t planned 5.1 yet! It’s too soon.
\nThere are also features that we want to do that people haven’t asked for, but that we think are cool. \uD83C\uDFB8
\nThe take-away from this article should be: we’re being very careful about designing and implementing new features, because we have to make sure NetNewsWire doesn’t lose what makes it special.
\nBut we are doing new features, because there are so many things that can make the app even better — we can make it better for current users and we can bring in new users.
"},"alternate":[{"href":"https://inessential.com/2019/08/31/netnewswire_5_feature_requests","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5 Feature Requests","published":1567278518000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/29/follow_through","fingerprint":"444937b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98d:18991ffa","summary":{"direction":"ltr","content":"Decades ago, when I was working for Dave Winer at UserLand, I learned about the concept of follow-through after a major release.
\nIf you’re an app maker, it might seem like your goal is to get to release day. Get the app done, make it available, publish an announcement, and then get back to coding. Let the world do what it’s going to do.
\nOne bang, and then back to work, in other words.
\nBut that’s not going to maximize your chances for a good release. You need to follow through — you need to keep going.
\nSome of the things you might do, in no particular order:
\n- \n
- Publish tips on using your app — one a day or so \n
- Update your website with feedback, testimonials, and good reviews \n
- Be available and communicative about your app \n
- Go on some podcasts \n
- Write about how release day went \n
- Write about plans for the x.0.1 version \n
- Field bug reports and feature requests gratefully \n
- Thank reviewers who’ve done a good job \n
- Make it as easy as possible for reporters and reviewers to get access to your app and to you \n
- Work to build a community of customers, on Slack or similar \n
I’m sure you can think of more things to do — the above isn’t everything, and every app is different.
\nBut the key is that you don’t just do the release and then stop. Instead, show that you‘re responsive, show that your app has momentum, show that you care enough to keep showing up.
\nFor me, at least, this is the fun part. I realize that’s not true for everybody — but you should do it anyway. \uD83C\uDFA9
"},"alternate":[{"href":"https://inessential.com/2019/08/29/follow_through","type":"text/html"}],"crawled":1569829821629,"title":"Follow-Through","published":1567110304000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/28/daniel_figures_out_one_of_the_two_crashi","fingerprint":"669e65c4","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98c:18991ffa","summary":{"direction":"ltr","content":"We have a few reports of a crash where the add-feed-sheet window doesn’t load. There’s a line of code with window! — because of course we expect the window to have been loaded — and it crashes right there.
This crash made zero sense to me, but Daniel Jalkut figured out the most likely cause and was able to reproduce it: it’s because the person has moved the app (from one folder to another) after launching it, while it’s running, and the nib-loading machinery can’t find the nib, because it’s moved along with the app.
\nTip: if you’re going to move an app, quit it first, then move it, and then re-launch it!
\nAt any rate: our fix for this will be to load that sheet on startup, and then recycle it on each use. This fix will go into NetNewsWire 5.0.1.
\nThis just fixes the bug with this one nib, though. A more systematic fix — maybe just a warning to the user suggesting they quit and re-launch — would be a good idea.
\nFile under “bugs iOS developers never have to worry about.” \uD83D\uDC07
\nPS We have a 5.0.1 beta milestone now.
"},"alternate":[{"href":"https://inessential.com/2019/08/28/daniel_figures_out_one_of_the_two_crashi","type":"text/html"}],"crawled":1569829821629,"title":"Daniel Figures Out One of the Two Crashing Bugs","published":1567022746000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/27/how_release_day_went","fingerprint":"2394a816","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98b:18991ffa","summary":{"direction":"ltr","content":"Yesterday was a great day! A few things to note, in no particular order:
\nNetNewsWire got some press coverage, including a well-done review in MacStories.
\nWe got a lot of feature requests, but no bug reports.
\nExcept that we did get a single-digit number of crash logs. On investigation, I found two distinct backtraces — we’ll need to fix those. The thing is, there’s no freakin’ way the app should crash in those spots. Except that, obviously, it can. Rarely, but it happens.
\nThe servers started timing-out at one point during the day. I contacted DreamHost support and they fixed things (and told me that the fixes they applied should prevent this in the future).
\nThere were a number of nice blog posts and tweets about NetNewsWire, which was awesome. After working so hard for so long, it’s great when people appreciate the app. We don’t get paid in money, after all. \uD83D\uDC23
\nI have no idea how many downloads of the app there were. GitHub is hosting the download, via its releases feature, and I don’t see a way to find out how many times it’s been downloaded. Which is totally fine with me.
\n* * *
\nI should say something more about the no-bug-reports. There’s no special magic or talent or anything to this — there’s just the willingness to say that we’re not going to ship until we’ve got the bugs out, and then sticking to that.
\nThis is a matter of pride and ethics, for sure, but there’s another dimension: since the app is open source, it’s written by volunteers (including me), and we have no dedicated support team. Any time we spend fielding bug reports is time taken away from working on the next feature.
\nMaking apps — even, or especially, free apps — is an exercise in economics. With free apps, the economics are even more constrained, because nobody is going to hire even a part-time support person. So we do everything we can do keep costs down — especially time costs.
\nPlus — buggy apps can be demoralizing to the people who work on them. Part of my job is to make sure people are proud and happy to work on the app. And that means making sure everyone knows we’re super-serious about doing our best to never ship bugs.
"},"alternate":[{"href":"https://inessential.com/2019/08/27/how_release_day_went","type":"text/html"}],"crawled":1569829821629,"title":"How Release Day Went","published":1566937707000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/26/netnewswire_5_0_now_available","fingerprint":"175d2cdb","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98a:18991ffa","summary":{"direction":"ltr","content":"\nNetNewsWire 5.0 is shipping!
\nIn case you haven’t been following along until just now: NetNewsWire is an open source RSS reader for Mac. It’s free! You can just download it and use it. No strings.
\nIt’s designed to be stable, fast, and free of bugs. It doesn’t have a lot of features yet, and that’s because we prioritized quality over features. We will be adding more features, of course, but not quickly. We’re also working on an iOS app.
\nIt syncs using Feedbin. We’ll support more systems in the future (as many as possible).
\nI hope you like it!
\nSome links…
\n- \n
- NetNewsWire home page \n
- Screenshots \n
- NetNewsWire blog \n
- Frequently Asked Questions \n
- NetNewsWire Help \n
- NetNewsWire History \n
- NetNewsWire Slack group \n
- How to Support NetNewsWire \n
- NetNewsWire repository on GitHub \n
Thanks to so many people
\nI want to especially thank Sheila Simmons and my family and friends.
\nThis release took five years to make, and for four of those years it wasn’t even called NetNewsWire. It was just a year ago that I got the name NetNewsWire back from Black Pixel — and I thank them again for their wonderful generosity.
\nI also want to thank Brad Ellis for making the beautiful app icon and toolbar icons. Thanks to our major code contributors: Maurice Parker, Olof Hellman, and Daniel Jalkut. Thanks to Ryan Dotson for writing the Help book. Thanks to Joe Heck for looking after infrastructure issues (especially continuous integration).
\nThanks to my co-workers and friends at The Omni Group (which is a wonderful place to work). Thanks to the ever-patient and ever-awesome NetNewsWire beta testers on the Slack group and elsewhere.
\nAnd thanks to everyone who’s ever used the app in its 17-years-and-counting run. Because of you, NetNewsWire has been, and remains, the thrill of my career.
"},"alternate":[{"href":"https://inessential.com/2019/08/26/netnewswire_5_0_now_available","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5.0 Now Available","published":1566834451000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/22/end_of_the_line_for_netnewswire_3_3_2","fingerprint":"e30daaa8","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d989:18991ffa","summary":{"direction":"ltr","content":"This is a little bit of bad news. It’s not my intention, and it’s not what I want to happen — but NetNewsWire 3.3.2 apparently does not launch in the next version of macOS (10.15, Catalina).
\nIt links to the PubSub framework, which is not included with the next macOS.
\nNetNewsWire 3.3.2 was the last release of the full version that I worked on, before selling NetNewsWire to Black Pixel, and I’ve heard from lots of people that they’ve been using it ever since. They never switched.
\nI would rather it continued working forever, but that’s not to be. Not my choice. Sorry about that!
"},"alternate":[{"href":"https://inessential.com/2019/08/22/end_of_the_line_for_netnewswire_3_3_2","type":"text/html"}],"crawled":1569829821629,"title":"End of the Line for NetNewsWire 3.3.2","published":1566515704000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/21/the_netnewswire_blog_has_the_details_on_","fingerprint":"c583e740","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d988:18991ffa","summary":{"direction":"ltr","content":"The NetNewsWire blog has the details on NetNewsWire 5.0b5 — which should be the last beta.
\nStill planning to do the 5.0 final release Monday morning, which really means doing the release on Sunday and pushing an announcement to this blog Monday morning. :)
\nThe last things on my to-do list are actually writing that announcement and doing screenshots for the NetNewsWire web page. Easy. \uD83D\uDC2F
"},"alternate":[{"href":"https://inessential.com/2019/08/21/the_netnewswire_blog_has_the_details_on_","type":"text/html"}],"crawled":1569829821629,"published":1566452581000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/20/immunization","fingerprint":"39a4bdb0","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d987:18991ffa","summary":{"direction":"ltr","content":"Before every major release I like to try and think of everything mean that people might say about the app. It’s fun!
\nSo we just went through this exercise on the NetNewsWire Slack group. Here’s a taste:
\n- \n
- This took five years? I could write an RSS parser in a weekend. \n
- Can’t get my Twitter and Facebook feeds. Whatever. \n
- Doesn’t work with my Usenet host. \n
- The information density of the timeline is… lacking. What the hell. \n
- Not truly open source since it’s on a Mac. \n
- Not truly open source since it’s not GPL. \n
- No vim keys. Why bother. \n
- Regular people will never use an RSS reader. What’s the point? \n
- Brent’s last good idea was in 2002. Consider this a textbook case of coasting. \n
- Great app. Too bad RSS died with Google Reader. \n
- It totally didn’t pick up my subscriptions from the earlier version. How is this an upgrade? \n
- When does a 5.0 have fewer features than a 3.0? When it’s NetNewsWire. \n
- The echo chamber will love this app. They always do. \n
- Free app. Continues the race to the bottom. Pour one out for Silvio Rizzi. \n
- No way to send to Instapaper. Fuck it. \n
- Brent Simmons can’t stop pursuing a technology that even Google famously admitted was not worth bothering with. \n
- If this app took five years, imagine how long it will take before it will actually sync with Feedly. \n
- Sure it’s free, but I bet the Feedbin people paid them off, because the only way to sync is to pay money to Feedbin. \n
- No iCloud sync? Jerks. \n
- No iOS app. The revolution happened on mobile, Brant. What the actual fuck. \n
- Shoulda been Catalyst. Dinosaurs wrote this app. \n
- Not on the Mac App Store? I guess they don’t want users. \n
- I would totally use this if it had just this one [feature x], which I can’t believe they shipped without. (Multiply this comment by 100, with a different feature x each time.) \n
- Area Man Can’t Let RSS Go \n
Some feedback will be factually inaccurate, but we like to imagine that too:
\n- \n
- I remember using NetNewsWire on OS 9, and it hasn’t really improved since then. They should make it a Cocoa app. \n
- Doesn’t work with web comics. POS \n
- Doesn’t support 10.5. \n
- It should be free. \n
- You’d think they would have updated the design — but it looks exactly like NetNewsWire of old. \n
- Why the hell would they build on that aging code base from Black Pixel? I heard it doesn’t even use ARC. \n
- No way to sync? What’s their actual problem? \n
See? The actual feedback will be nicer than the stuff we thought up. This provides a bit of immunization. :)
\nBut, also, there will be negative feedback we didn’t imagine. That’s the gold!
\n* * *
\nBonus from Daniel Jalkut, but not actually a criticism:
\n\n"},"alternate":[{"href":"https://inessential.com/2019/08/20/immunization","type":"text/html"}],"crawled":1569829821629,"title":"Immunization","published":1566332363000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]},{"originId":"https://inessential.com/2019/08/19/i_think_were_still_on_track_for_releasin","fingerprint":"592043f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d986:18991ffa","summary":{"direction":"ltr","content":"Can’t innovate, my RSS.
\n
I think we’re still on track for releasing NetNewsWire 5.0 Monday, August 26. There will be one more beta before then.
\nI’ll be available for podcasts, interviews-via-email, etc. If you’d like to set something up, email me or DM me on Twitter.
"},"alternate":[{"href":"https://inessential.com/2019/08/19/i_think_were_still_on_track_for_releasin","type":"text/html"}],"crawled":1569829821629,"published":1566259329000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","label":"Must Read"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fc09f383-5a9a-4daa-a575-3efc1733b173","label":"NewCollection"}]}]} \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyCheckpointOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyCheckpointOperationTests.swift new file mode 100644 index 000000000..074b8f7cc --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyCheckpointOperationTests.swift @@ -0,0 +1,61 @@ +// +// FeedlyCheckpointOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 25/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +class FeedlyCheckpointOperationTests: XCTestCase { + + class TestDelegate: FeedlyCheckpointOperationDelegate { + + var didReachCheckpointExpectation: XCTestExpectation? + + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { + didReachCheckpointExpectation?.fulfill() + } + } + + func testCallback() { + let delegate = TestDelegate() + delegate.didReachCheckpointExpectation = expectation(description: "Did Reach Checkpoint") + + let operation = FeedlyCheckpointOperation() + operation.checkpointDelegate = delegate + + let didFinishExpectation = expectation(description: "Did Finish") + operation.completionBlock = { + didFinishExpectation.fulfill() + } + + OperationQueue.main.addOperation(operation) + + waitForExpectations(timeout: 2) + } + + func testCancellation() { + let didReachCheckpointExpectation = expectation(description: "Did Reach Checkpoint") + didReachCheckpointExpectation.isInverted = true + + let delegate = TestDelegate() + delegate.didReachCheckpointExpectation = didReachCheckpointExpectation + + let operation = FeedlyCheckpointOperation() + operation.checkpointDelegate = delegate + + let didFinishExpectation = expectation(description: "Did Finish") + operation.completionBlock = { + didFinishExpectation.fulfill() + } + + OperationQueue.main.addOperation(operation) + + operation.cancel() + + waitForExpectations(timeout: 1) + } +} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift new file mode 100644 index 000000000..408e093db --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyCreateFeedsForCollectionFoldersOperationTests.swift @@ -0,0 +1,195 @@ +// +// FeedlyCreateFeedsForCollectionFoldersOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 22/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +class FeedlyCreateFeedsForCollectionFoldersOperationTests: XCTestCase { + + private var account: Account! + private let support = FeedlyTestSupport() + + override func setUp() { + super.setUp() + account = support.makeTestAccount() + } + + override func tearDown() { + if let account = account { + support.destroy(account) + } + super.tearDown() + } + + class FeedsAndFoldersProvider: FeedlyFeedsAndFoldersProviding { + var feedsAndFolders = [([FeedlyFeed], Folder)]() + } + + func testAddFeeds() { + let feedsForFolderOne = [ + FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil), + FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil) + ] + + let feedsForFolderTwo = [ + FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil), + FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil), + ] + + let folderOne: (name: String, id: String) = ("FolderOne", "folder/1") + let folderTwo: (name: String, id: String) = ("FolderTwo", "folder/2") + let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)] + + let provider = FeedsAndFoldersProvider() + provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in + let accountFolder = account.ensureFolder(with: folder.name)! + accountFolder.externalID = folder.id + return (feeds, accountFolder) + } + + let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log) + let completionExpectation = expectation(description: "Did Finish") + createFeeds.completionBlock = { + completionExpectation.fulfill() + } + + XCTAssertTrue(account.flattenedWebFeeds().isEmpty, "Expected empty account.") + + OperationQueue.main.addOperation(createFeeds) + + waitForExpectations(timeout: 2) + + let feedIds = Set([feedsForFolderOne, feedsForFolderTwo] + .flatMap { $0 } + .map { $0.feedId }) + + let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo] + .flatMap { $0 } + .map { $0.title }) + + let accountFeeds = account.flattenedWebFeeds() + let ingestedIds = Set(accountFeeds.map { $0.webFeedID }) + let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay }) + + let missingIds = feedIds.subtracting(ingestedIds) + let missingTitles = feedTitles.subtracting(ingestedTitles) + + XCTAssertTrue(missingIds.isEmpty, "Failed to ingest feeds with these ids.") + XCTAssertTrue(missingTitles.isEmpty, "Failed to ingest feeds with these titles.") + + let expectedFolderAndFeedIds = namesAndFeeds + .sorted { $0.0.id < $1.0.id } + .map { folder, feeds -> [String: [String]] in + return [folder.id: feeds.map { $0.feedId }.sorted(by: <)] + } + + let ingestedFolderAndFeedIds = (account.folders ?? Set()) + .sorted { $0.externalID! < $1.externalID! } + .compactMap { folder -> [String: [String]]? in + return [folder.externalID!: folder.topLevelWebFeeds.map { $0.webFeedID }.sorted(by: <)] + } + + XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds in their corresponding folders.") + } + + func testRemoveFeeds() { + let folderOne: (name: String, id: String) = ("FolderOne", "folder/1") + let folderTwo: (name: String, id: String) = ("FolderTwo", "folder/2") + let feedToRemove = FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil) + + var feedsForFolderOne = [ + feedToRemove, + FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil) + ] + + var feedsForFolderTwo = [ + feedToRemove, + FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil), + ] + + // Add initial content. + do { + let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)] + + let provider = FeedsAndFoldersProvider() + provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in + let accountFolder = account.ensureFolder(with: folder.name)! + accountFolder.externalID = folder.id + return (feeds, accountFolder) + } + + let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log) + let completionExpectation = expectation(description: "Did Finish") + createFeeds.completionBlock = { + completionExpectation.fulfill() + } + + XCTAssertTrue(account.flattenedWebFeeds().isEmpty, "Expected empty account.") + + OperationQueue.main.addOperation(createFeeds) + + waitForExpectations(timeout: 2) + } + + feedsForFolderOne.removeAll { $0.id == feedToRemove.id } + feedsForFolderTwo.removeAll { $0.id == feedToRemove.id } + let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)] + + let provider = FeedsAndFoldersProvider() + provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in + let accountFolder = account.ensureFolder(with: folder.name)! + accountFolder.externalID = folder.id + return (feeds, accountFolder) + } + + let removeFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log) + let completionExpectation = expectation(description: "Did Finish") + removeFeeds.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(removeFeeds) + + waitForExpectations(timeout: 2) + + let feedIds = Set([feedsForFolderOne, feedsForFolderTwo] + .flatMap { $0 } + .map { $0.feedId }) + + let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo] + .flatMap { $0 } + .map { $0.title }) + + let accountFeeds = account.flattenedWebFeeds() + let ingestedIds = Set(accountFeeds.map { $0.webFeedID }) + let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay }) + + XCTAssertEqual(ingestedIds.count, feedIds.count) + XCTAssertEqual(ingestedTitles.count, feedTitles.count) + + let missingIds = feedIds.subtracting(ingestedIds) + let missingTitles = feedTitles.subtracting(ingestedTitles) + + XCTAssertTrue(missingIds.isEmpty, "Failed to ingest feeds with these ids.") + XCTAssertTrue(missingTitles.isEmpty, "Failed to ingest feeds with these titles.") + + let expectedFolderAndFeedIds = namesAndFeeds + .sorted { $0.0.id < $1.0.id } + .map { folder, feeds -> [String: [String]] in + return [folder.id: feeds.map { $0.feedId }.sorted(by: <)] + } + + let ingestedFolderAndFeedIds = (account.folders ?? Set()) + .sorted { $0.externalID! < $1.externalID! } + .compactMap { folder -> [String: [String]]? in + return [folder.externalID!: folder.topLevelWebFeeds.map { $0.webFeedID }.sorted(by: <)] + } + + XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds to their corresponding folders.") + } +} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyGetCollectionsOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyGetCollectionsOperationTests.swift new file mode 100644 index 000000000..bf8e5bd80 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyGetCollectionsOperationTests.swift @@ -0,0 +1,92 @@ +// +// FeedlyGetCollectionsOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 21/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account +import os.log + +class FeedlyGetCollectionsOperationTests: XCTestCase { + + func testGetCollections() { + let support = FeedlyTestSupport() + let (transport, caller) = support.makeMockNetworkStack() + let jsonName = "feedly_collections_initial" + transport.testFiles["/v3/collections"] = "\(jsonName).json" + + let getCollections = FeedlyGetCollectionsOperation(service: caller, log: support.log) + let completionExpectation = expectation(description: "Did Finish") + getCollections.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getCollections) + + waitForExpectations(timeout: 2) + + let collections = support.testJSON(named: jsonName) as! [[String:Any]] + let labelsInJSON = Set(collections.map { $0["label"] as! String }) + let idsInJSON = Set(collections.map { $0["id"] as! String }) + + let labels = Set(getCollections.collections.map { $0.label }) + let ids = Set(getCollections.collections.map { $0.id }) + + let missingLabels = labelsInJSON.subtracting(labels) + let missingIds = idsInJSON.subtracting(ids) + + XCTAssertEqual(getCollections.collections.count, collections.count, "Mismatch between collections provided by operation and test JSON collections.") + XCTAssertTrue(missingLabels.isEmpty, "Collections with these labels did not have a corresponding \(FeedlyCollection.self) value with the same name.") + XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding \(FeedlyCollection.self) with the same id.") + + for collection in collections { + let collectionId = collection["id"] as! String + let collectionFeeds = collection["feeds"] as! [[String: Any]] + let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String }) + + for operationCollection in getCollections.collections where operationCollection.id == collectionId { + let feedIds = Set(operationCollection.feeds.map { $0.id }) + let missingIds = collectionFeedIds.subtracting(feedIds) + XCTAssertTrue(missingIds.isEmpty, "Feeds with these ids were not found in the \"\(operationCollection.label)\" \(FeedlyCollection.self).") + } + } + } + + func testGetCollectionsError() { + + class TestDelegate: FeedlyOperationDelegate { + var errorExpectation: XCTestExpectation? + var error: Error? + + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { + self.error = error + errorExpectation?.fulfill() + } + } + + let delegate = TestDelegate() + delegate.errorExpectation = expectation(description: "Did Fail With Expected Error") + + let support = FeedlyTestSupport() + let service = TestGetCollectionsService() + service.mockResult = .failure(URLError(.timedOut)) + + let getCollections = FeedlyGetCollectionsOperation(service: service, log: support.log) + getCollections.delegate = delegate + + let completionExpectation = expectation(description: "Did Finish") + getCollections.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getCollections) + + waitForExpectations(timeout: 2) + + XCTAssertNotNil(delegate.error) + XCTAssertTrue(getCollections.collections.isEmpty, "Collections should be empty.") + } +} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyGetStreamContentsOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyGetStreamContentsOperationTests.swift new file mode 100644 index 000000000..b31f66887 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyGetStreamContentsOperationTests.swift @@ -0,0 +1,131 @@ +// +// FeedlyGetStreamContentsOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 23/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +class FeedlyGetStreamContentsOperationTests: XCTestCase { + + private var account: Account! + private let support = FeedlyTestSupport() + + override func setUp() { + super.setUp() + account = support.makeTestAccount() + } + + override func tearDown() { + if let account = account { + support.destroy(account) + } + super.tearDown() + } + + func testGetStreamContentsFailure() { + let service = TestGetStreamContentsService() + let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") + + let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: service, continuation: nil, newerThan: nil, unreadOnly: nil) + + service.mockResult = .failure(URLError(.fileDoesNotExist)) + + let completionExpectation = expectation(description: "Did Finish") + getStreamContents.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getStreamContents) + + waitForExpectations(timeout: 2) + + XCTAssertNil(getStreamContents.stream) + } + + func testValuesPassingForGetStreamContents() { + let service = TestGetStreamContentsService() + let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") + + let continuation: String? = "abcdefg" + let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 86) + let unreadOnly: Bool? = true + + let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: service, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) + + let mockStream = FeedlyStream(id: "stream/1", updated: nil, continuation: nil, items: []) + service.mockResult = .success(mockStream) + service.getStreamContentsExpectation = expectation(description: "Did Call Service") + service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in + // Verify these values given to the opeartion are passed to the service. + XCTAssertEqual(serviceResource.id, resource.id) + XCTAssertEqual(serviceContinuation, continuation) + XCTAssertEqual(serviceNewerThan, newerThan) + XCTAssertEqual(serviceUnreadOnly, unreadOnly) + } + + let completionExpectation = expectation(description: "Did Finish") + getStreamContents.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getStreamContents) + + waitForExpectations(timeout: 2) + + guard let stream = getStreamContents.stream else { + XCTFail("\(FeedlyGetStreamContentsOperation.self) did not store the stream.") + return + } + + XCTAssertEqual(stream.id, mockStream.id) + XCTAssertEqual(stream.updated, mockStream.updated) + XCTAssertEqual(stream.continuation, mockStream.continuation) + + let streamIds = stream.items.map { $0.id } + let mockStreamIds = mockStream.items.map { $0.id } + XCTAssertEqual(streamIds, mockStreamIds) + } + + func testGetStreamContentsFromJSON() { + let support = FeedlyTestSupport() + let (transport, caller) = support.makeMockNetworkStack() + let jsonName = "feedly_macintosh_initial" + transport.testFiles["/v3/streams/contents"] = "\(jsonName).json" + + let resource = FeedlyCategoryResourceId(id: "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815") + let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: caller, continuation: nil, newerThan: nil, unreadOnly: nil) + + let completionExpectation = expectation(description: "Did Finish") + getStreamContents.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getStreamContents) + + waitForExpectations(timeout: 2) + + // verify entry providing and parsed item providing + guard let stream = getStreamContents.stream else { + return XCTFail("Expected to have stream.") + } + + let streamJSON = support.testJSON(named: jsonName) as! [String:Any] + + let id = streamJSON["id"] as! String + XCTAssertEqual(stream.id, id) + + let milliseconds = streamJSON["updated"] as! Double + let updated = Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000)) + XCTAssertEqual(stream.updated, updated) + + let continuation = streamJSON["continuation"] as! String + XCTAssertEqual(stream.continuation, continuation) + + support.check(getStreamContents.entries, correspondToStreamItemsIn: streamJSON) + support.check(stream.items, correspondToStreamItemsIn: streamJSON) + } +} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyGetStreamIdsOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyGetStreamIdsOperationTests.swift new file mode 100644 index 000000000..48b19432a --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyGetStreamIdsOperationTests.swift @@ -0,0 +1,116 @@ +// +// FeedlyGetStreamIdsOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 23/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +class FeedlyGetStreamIdsOperationTests: XCTestCase { + + private var account: Account! + private let support = FeedlyTestSupport() + + override func setUp() { + super.setUp() + account = support.makeTestAccount() + } + + override func tearDown() { + if let account = account { + support.destroy(account) + } + super.tearDown() + } + + func testGetStreamIdsFailure() { + let service = TestGetStreamIdsService() + let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") + + let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: service, continuation: nil, newerThan: nil, unreadOnly: nil) + + service.mockResult = .failure(URLError(.fileDoesNotExist)) + + let completionExpectation = expectation(description: "Did Finish") + getStreamIds.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getStreamIds) + + waitForExpectations(timeout: 2) + + XCTAssertNil(getStreamIds.streamIds) + } + + func testValuesPassingForGetStreamIds() { + let service = TestGetStreamIdsService() + let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") + + let continuation: String? = "gfdsa" + let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 1000) + let unreadOnly: Bool? = false + + let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: service, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) + + let mockStreamIds = FeedlyStreamIds(continuation: "1234", ids: ["item/1", "item/2", "item/3"]) + service.mockResult = .success(mockStreamIds) + service.getStreamIdsExpectation = expectation(description: "Did Call Service") + service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in + // Verify these values given to the opeartion are passed to the service. + XCTAssertEqual(serviceResource.id, resource.id) + XCTAssertEqual(serviceContinuation, continuation) + XCTAssertEqual(serviceNewerThan, newerThan) + XCTAssertEqual(serviceUnreadOnly, unreadOnly) + } + + let completionExpectation = expectation(description: "Did Finish") + getStreamIds.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getStreamIds) + + waitForExpectations(timeout: 2) + + guard let streamIds = getStreamIds.streamIds else { + XCTFail("\(FeedlyGetStreamIdsOperation.self) did not store the stream.") + return + } + + XCTAssertEqual(streamIds.continuation, mockStreamIds.continuation) + XCTAssertEqual(streamIds.ids, mockStreamIds.ids) + } + + func testGetStreamIdsFromJSON() { + let support = FeedlyTestSupport() + let (transport, caller) = support.makeMockNetworkStack() + let jsonName = "feedly_unreads_1000" + transport.testFiles["/v3/streams/ids"] = "\(jsonName).json" + + let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678") + let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: caller, continuation: nil, newerThan: nil, unreadOnly: nil) + + let completionExpectation = expectation(description: "Did Finish") + getStreamIds.completionBlock = { + completionExpectation.fulfill() + } + + OperationQueue.main.addOperation(getStreamIds) + + waitForExpectations(timeout: 2) + + guard let streamIds = getStreamIds.streamIds else { + return XCTFail("Expected to have a stream of identifiers.") + } + + let streamIdsJSON = support.testJSON(named: jsonName) as! [String:Any] + + let continuation = streamIdsJSON["continuation"] as! String + XCTAssertEqual(streamIds.continuation, continuation) + XCTAssertEqual(streamIds.ids, streamIdsJSON["ids"] as! [String]) + } +} diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlyLogoutOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlyLogoutOperationTests.swift new file mode 100644 index 000000000..95c3c3156 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/FeedlyLogoutOperationTests.swift @@ -0,0 +1,216 @@ +// +// FeedlyLogoutOperationTests.swift +// AccountTests +// +// Created by Kiel Gillard on 15/11/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +class FeedlyLogoutOperationTests: XCTestCase { + + private var account: Account! + private let support = FeedlyTestSupport() + + override func setUp() { + super.setUp() + account = support.makeTestAccount() + } + + override func tearDown() { + if let account = account { + support.destroy(account) + } + super.tearDown() + } + + private func getTokens(for account: Account) throws -> (accessToken: Credentials, refreshToken: Credentials) { + guard let accessToken = try account.retrieveCredentials(type: .oauthAccessToken), let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) else { + XCTFail("Unable to retrieve access and/or refresh token from account.") + throw CredentialsError.incompleteCredentials + } + return (accessToken, refreshToken) + } + + class TestFeedlyLogoutService: FeedlyLogoutService { + var mockResult: Result