Author SHA1 Message Date
CedricandClaude Opus 5.5 84cb11e4dd Hide paid Substack newsletter posts with the paid filter (#7)
Substack cuts paid-subscriber posts short in the feed and ends their
content with a "Read more" link to the post itself. PaywallService.isPaid
now recognizes that ending, so "hide paid articles" hides these posts in
Today and the Learn tab and counts them as hidden, without a page request.
Free posts that ask readers to subscribe or end with the subscribe widget
are not affected.

Tests use trimmed real items from the Interconnects and Ahead of AI feeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:57:48 +02:00
CedricandClaude Opus 5.5 4f035e4bb4 Merge ticket/6-learning-catalog into feature/source-kinds (#6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:51:33 +02:00
CedricandClaude Opus 5.5 b634118ed8 Add the curated Learning sources (#6)
Fifteen English Learning sources join ByteByteGo in the catalog: System
Design One, The Pragmatic Engineer (blog), Martin Fowler, Julia Evans,
Dan Luu, The Cloudflare Blog, Netflix Technology Blog, Engineering at
Meta, GitHub Blog: Engineering, Latent Space, Import AI, Interconnects,
Ahead of AI and Lilian Weng (on by default), and InfoQ (off).

Tests pin the Learning catalog, check that existing installs get the
new on-by-default Sources and that the Learn tab shows their posts from
the last 14 days. A live test times a full refresh. The README gets a
Learn feature bullet and a Learning sources table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:50:50 +02:00
11 changed files with 402 additions and 4 deletions

No files matched your search

+36 -1
View File
@@ -8,6 +8,9 @@ or write a daily briefing. It's meant to replace the n8n → Discord workflow.
- **Today**: all articles from the enabled sources in the last 12/24/48 h, newest
first. Filter by language or source, pull to refresh.
- **Learn**: articles from the enabled Learning sources (engineering blogs,
system design explainers, AI research write-ups) from the last 14 days, newest
first. Learning sources aren't part of the Briefing.
- **Reading in the app**: tapping an article opens a clean **reader view** (text,
headings, lists, quotes, images) without ads or cookie banners. The globe icon
switches to the **original page** in an in-app WebView, for paywalls or sites
@@ -22,7 +25,9 @@ or write a daily briefing. It's meant to replace the n8n → Discord workflow.
- **Paid articles and ads hidden**: heise+ and Golem `(g+)` are recognized by
their title; SPIEGEL+ and t3n Pro by a marker in the article page, which the app
checks once per new article in the background (it reads only until the marker,
and remembers results for 14 days). Sponsored posts (`Anzeige:`) are hidden too.
and remembers results for 14 days). Paid-subscriber posts from Substack newsletters
(Interconnects, Ahead of AI, …) are recognized in the feed itself, where Substack
cuts them off with a "Read more" link. Sponsored posts (`Anzeige:`) are hidden too.
The feed header shows how many were hidden; both filters can be turned off.
- **Ad and cookie-banner blocking** in the original-page view: requests to ~80
ad/tracker hosts and the common consent managers (Sourcepoint, OneTrust,
@@ -181,6 +186,36 @@ page. Hacker News and Lobsters link to articles all over the web, so reader view
depends on the linked site. Techmeme links to its own summary page, which only
has the teaser; the original page links on to the full story.
## Learning sources
Evergreen reading for building skills: research, engineering depth and system
design rather than product news. The Learn tab shows their posts from the last
14 days. All are English; ★ marks the ones enabled by default.
| | Source | Feed | Focus |
|---|---|---|---|
| 🇬🇧 ★ | ByteByteGo | `blog.bytebytego.com/feed` | System design explained with diagrams |
| 🇬🇧 ★ | System Design One | `newsletter.systemdesign.one/feed` | System design case studies of real systems |
| 🇬🇧 ★ | The Pragmatic Engineer (blog) | `blog.pragmaticengineer.com/rss/` | How big tech and startups build software (free posts) |
| 🇬🇧 ★ | Martin Fowler | `martinfowler.com/feed.atom` | Software architecture, refactoring, delivery |
| 🇬🇧 ★ | Julia Evans | `jvns.ca/atom.xml` | Systems, networking and Unix tools, explained plainly |
| 🇬🇧 ★ | Dan Luu | `danluu.com/atom.xml` | Long, data-driven essays on computers and engineering |
| 🇬🇧 ★ | The Cloudflare Blog | `blog.cloudflare.com/rss/` | Networking, security, incident write-ups |
| 🇬🇧 ★ | Netflix Technology Blog | `netflixtechblog.com/feed` | Distributed systems, data, streaming at scale |
| 🇬🇧 ★ | Engineering at Meta | `engineering.fb.com/feed/` | Infrastructure, AI, production engineering |
| 🇬🇧 ★ | GitHub Blog: Engineering | `github.blog/engineering/feed/` | How GitHub builds and runs GitHub |
| 🇬🇧 ★ | Latent Space | `www.latent.space/feed` | AI engineering: interviews and deep dives |
| 🇬🇧 ★ | Import AI | `importai.substack.com/feed` | Weekly AI research digest by Jack Clark |
| 🇬🇧 ★ | Interconnects | `www.interconnects.ai/feed` | Language model training and research |
| 🇬🇧 ★ | Ahead of AI | `magazine.sebastianraschka.com/feed` | LLM research explained by Sebastian Raschka |
| 🇬🇧 ★ | Lilian Weng | `lilianweng.github.io/index.xml` | Thorough surveys of machine learning topics |
| 🇬🇧 | InfoQ | `feed.infoq.com/` | Architecture and engineering talks, high volume |
Left out on purpose: The Pragmatic Engineer newsletter (mostly paid), One Useful
Thing (applied AI), vendor blogs (OpenAI, DeepMind, Hugging Face) and Chip Huyen
(inactive since January 2025). Some feeds are long (Dan Luu ~130 posts, Lilian
Weng ~50); the 14-day window keeps the list short.
## Project layout
```
+149
View File
@@ -212,6 +212,155 @@ const defaultSources = <NewsSource>[
description: 'System design explained with diagrams',
enabledByDefault: true,
),
NewsSource(
id: 'systemdesignone',
name: 'System Design One',
feedUrl: 'https://newsletter.systemdesign.one/feed',
homepage: 'https://newsletter.systemdesign.one',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'System design case studies of real systems',
enabledByDefault: true,
),
NewsSource(
id: 'pragmaticengineer-blog',
name: 'The Pragmatic Engineer (blog)',
feedUrl: 'https://blog.pragmaticengineer.com/rss/',
homepage: 'https://blog.pragmaticengineer.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'How big tech and startups build software (free posts)',
enabledByDefault: true,
),
NewsSource(
id: 'martinfowler',
name: 'Martin Fowler',
feedUrl: 'https://martinfowler.com/feed.atom',
homepage: 'https://martinfowler.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Software architecture, refactoring, delivery practices',
enabledByDefault: true,
),
NewsSource(
id: 'juliaevans',
name: 'Julia Evans',
feedUrl: 'https://jvns.ca/atom.xml',
homepage: 'https://jvns.ca',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Systems, networking and Unix tools, explained plainly',
enabledByDefault: true,
),
NewsSource(
id: 'danluu',
name: 'Dan Luu',
feedUrl: 'https://danluu.com/atom.xml',
homepage: 'https://danluu.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Long, data-driven essays on computers and engineering',
enabledByDefault: true,
),
NewsSource(
id: 'cloudflare-blog',
name: 'The Cloudflare Blog',
feedUrl: 'https://blog.cloudflare.com/rss/',
homepage: 'https://blog.cloudflare.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Networking, security and incident write-ups in depth',
enabledByDefault: true,
),
NewsSource(
id: 'netflix-techblog',
name: 'Netflix Technology Blog',
feedUrl: 'https://netflixtechblog.com/feed',
homepage: 'https://netflixtechblog.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Distributed systems, data and streaming at scale',
enabledByDefault: true,
),
NewsSource(
id: 'meta-engineering',
name: 'Engineering at Meta',
feedUrl: 'https://engineering.fb.com/feed/',
homepage: 'https://engineering.fb.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Infrastructure, AI and production engineering at Meta',
enabledByDefault: true,
),
NewsSource(
id: 'github-engineering',
name: 'GitHub Blog: Engineering',
feedUrl: 'https://github.blog/engineering/feed/',
homepage: 'https://github.blog/engineering/',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'How GitHub builds and runs GitHub',
enabledByDefault: true,
),
NewsSource(
id: 'latent-space',
name: 'Latent Space',
feedUrl: 'https://www.latent.space/feed',
homepage: 'https://www.latent.space',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'AI engineering: interviews and deep dives',
enabledByDefault: true,
),
NewsSource(
id: 'import-ai',
name: 'Import AI',
feedUrl: 'https://importai.substack.com/feed',
homepage: 'https://importai.substack.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Weekly AI research digest by Jack Clark',
enabledByDefault: true,
),
NewsSource(
id: 'interconnects',
name: 'Interconnects',
feedUrl: 'https://www.interconnects.ai/feed',
homepage: 'https://www.interconnects.ai',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Language model training and research, analysed',
enabledByDefault: true,
),
NewsSource(
id: 'ahead-of-ai',
name: 'Ahead of AI',
feedUrl: 'https://magazine.sebastianraschka.com/feed',
homepage: 'https://magazine.sebastianraschka.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'LLM research explained, by Sebastian Raschka',
enabledByDefault: true,
),
NewsSource(
id: 'lilianweng',
name: 'Lilian Weng',
feedUrl: 'https://lilianweng.github.io/index.xml',
homepage: 'https://lilianweng.github.io',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Thorough surveys of machine learning topics',
enabledByDefault: true,
),
NewsSource(
id: 'infoq',
name: 'InfoQ',
feedUrl: 'https://feed.infoq.com/',
homepage: 'https://www.infoq.com',
language: SourceLanguage.en,
kind: SourceKind.learning,
description: 'Software architecture and engineering talks, high volume',
),
];
/// The curated sources before the app remembered which ones it had seen.
+15 -1
View File
@@ -9,6 +9,8 @@ import 'storage.dart';
/// Recognizes paid and sponsored articles so the feed can hide them.
///
/// Most paid articles are marked in the title (heise+, Golem's "(g+)").
/// Substack newsletters cut paid posts short in the feed and end them with a
/// "Read more" link to the post itself, so those are recognized from the feed.
/// SPIEGEL and t3n only mark them in the page's schema.org data
/// (`"isAccessibleForFree": false`), so for those sources each new article is
/// checked once in the background and the result is remembered.
@@ -33,6 +35,14 @@ class PaywallService {
caseSensitive: false,
);
/// What Substack appends to a paid post's feed content in place of the rest:
/// `<p><a href="{post url}">Read more</a></p>`, as the very end. Free posts
/// end with their own text, even when they ask readers to subscribe.
static final _substackCut = RegExp(
r'<p>\s*<a href="([^"]+)">\s*Read more\s*</a>\s*</p>\s*$',
caseSensitive: false,
);
static final _marker =
RegExp(r'''"isAccessibleForFree"\s*:\s*"?(true|false)"?''', caseSensitive: false);
@@ -41,7 +51,11 @@ class PaywallService {
static const _concurrency = 6;
static const _keepFor = Duration(days: 14);
bool isPaid(Article a) => paidTitle.hasMatch(a.title) || (_checks[a.link]?.paid ?? false);
bool isPaid(Article a) =>
paidTitle.hasMatch(a.title) || _isCutSubstackPost(a) || (_checks[a.link]?.paid ?? false);
static bool _isCutSubstackPost(Article a) =>
_substackCut.firstMatch(a.contentHtml)?.group(1) == a.link;
bool isSponsored(Article a) => sponsoredTitle.hasMatch(a.title);
+34
View File
@@ -43,6 +43,40 @@ void main() {
expect(ids, isNot(contains('engadget')));
});
test('the Learning sources are the curated set, all English, with the agreed defaults', () {
final learning = {
for (final s in defaultSources)
if (s.kind == SourceKind.learning) s.name: (s.feedUrl, s.language, s.enabledByDefault),
};
const en = SourceLanguage.en;
expect(learning, {
'ByteByteGo': ('https://blog.bytebytego.com/feed', en, true),
'System Design One': ('https://newsletter.systemdesign.one/feed', en, true),
'The Pragmatic Engineer (blog)': ('https://blog.pragmaticengineer.com/rss/', en, true),
'Martin Fowler': ('https://martinfowler.com/feed.atom', en, true),
'Julia Evans': ('https://jvns.ca/atom.xml', en, true),
'Dan Luu': ('https://danluu.com/atom.xml', en, true),
'The Cloudflare Blog': ('https://blog.cloudflare.com/rss/', en, true),
'Netflix Technology Blog': ('https://netflixtechblog.com/feed', en, true),
'Engineering at Meta': ('https://engineering.fb.com/feed/', en, true),
'GitHub Blog: Engineering': ('https://github.blog/engineering/feed/', en, true),
'Latent Space': ('https://www.latent.space/feed', en, true),
'Import AI': ('https://importai.substack.com/feed', en, true),
'Interconnects': ('https://www.interconnects.ai/feed', en, true),
'Ahead of AI': ('https://magazine.sebastianraschka.com/feed', en, true),
'Lilian Weng': ('https://lilianweng.github.io/index.xml', en, true),
'InfoQ': ('https://feed.infoq.com/', en, false),
});
});
test('every Learning source says what it is for and links its homepage', () {
for (final s in defaultSources.where((s) => s.kind == SourceKind.learning)) {
expect(s.description, isNotEmpty, reason: s.name);
expect(Uri.parse(s.homepage).isScheme('https'), isTrue, reason: s.name);
}
});
test('every Source has its own id', () {
final ids = defaultSources.map((s) => s.id).toList();
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ahead of AI]]></title><link>https://magazine.sebastianraschka.com</link><generator>Substack</generator>
<!-- Real items from https://magazine.sebastianraschka.com/feed (October 2026), content:encoded trimmed to its first paragraph and its end. -->
<!-- Paid post (audience "only_paid" on its page): Substack cuts the content and ends it with a "Read more" link to the post. -->
<item><title><![CDATA[Building an AI Text Detector From Scratch]]></title><description><![CDATA[An End-to-End Project With Dataset Construction, Model Training, Local Deployment, and RLVR]]></description><link>https://magazine.sebastianraschka.com/p/ai-detector-from-scratch</link><guid isPermaLink="false">https://magazine.sebastianraschka.com/p/ai-detector-from-scratch</guid><dc:creator><![CDATA[Sebastian Raschka, PhD]]></dc:creator><pubDate>Sat, 15 Aug 2026 11:54:24 GMT</pubDate><content:encoded><![CDATA[<p>Substack recently launched its AI detector feature in the UI, which is super interesting.</p><p>For this, we are going to fine-tune a DistilBERT classifier (similar to what I described in one of my early Substack articles, <a href="https://magazine.sebastianraschka.com/p/finetuning-large-language-models">Finetuning Large Language Models</a>), but more details on that later when we get to that stage.</p><p></p>
<p>
<a href="https://magazine.sebastianraschka.com/p/ai-detector-from-scratch">
Read more
</a>
</p>
]]></content:encoded></item>
<!-- Free post (audience "everyone") whose footer asks readers to become paid subscribers. -->
<item><title><![CDATA[Controlling Reasoning Effort in LLMs]]></title><description><![CDATA[How LLMs Learn Low-, Medium-, and High-Effort Reasoning Modes]]></description><link>https://magazine.sebastianraschka.com/p/controlling-reasoning-effort-in-llms</link><guid isPermaLink="false">https://magazine.sebastianraschka.com/p/controlling-reasoning-effort-in-llms</guid><dc:creator><![CDATA[Sebastian Raschka, PhD]]></dc:creator><pubDate>Sat, 18 Jul 2026 11:16:09 GMT</pubDate><content:encoded><![CDATA[<p><span>It has been almost two years since OpenAI released o1, a model that popularized the idea of LLM-based reasoning models. DeepSeek-R1 followed about four months later, together with details of a reinforcement learning with verifiable rewards (RLVR) recipe to train such reasoning models.</span></p><p>If you liked my previous <a href="https://amzn.to/4fqvn0D">Build a Large Language Model (From Scratch)</a> book, this is essentially a sequel implementing inference-time scaling techniques and reinforcement learning algorithms from scratch.</p><p>And if you want to support future long-form articles like this one, consider <a href="https://magazine.sebastianraschka.com/subscribe">becoming a paid subscriber</a>. It helps me keep writing these independent deep dives and sharing the accompanying code, figures, and experiments.</p><p></p>]]></content:encoded></item>
<!-- Free post (audience "everyone") that ends with Substack's subscribe widget. -->
<item><title><![CDATA[GPT-6 Astra, Looped Transformers, and Hidden Reasoning]]></title><description><![CDATA[A Look at Recurrent Depth, Hidden Chains of Thought, and Recent Research on Looping Transformer Blocks]]></description><link>https://magazine.sebastianraschka.com/p/gpt-6-astra-looped-transformers-and</link><guid isPermaLink="false">https://magazine.sebastianraschka.com/p/gpt-6-astra-looped-transformers-and</guid><dc:creator><![CDATA[Sebastian Raschka, PhD]]></dc:creator><pubDate>Wed, 09 Sep 2026 11:14:25 GMT</pubDate><content:encoded><![CDATA[<p>A lot has happened in the last few weeks. I am sure that OpenAI&#8217;s GPT-6 Astra is top of mind for everyone right now. In particular, thoughts on its performance, the looped transformer/recurrent depth aspects, and rumors that Astra is &#8220;hiding&#8221; its reasoning trace (i.e., chain of thought).</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://magazine.sebastianraschka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item>
</channel></rss>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Interconnects AI]]></title><link>https://www.interconnects.ai</link><generator>Substack</generator>
<!-- Real items from https://www.interconnects.ai/feed (October 2026), content:encoded trimmed to its first paragraph and its end. -->
<!-- Paid post (audience "only_paid" on its page). -->
<item><title><![CDATA[Lessons from the hacks]]></title><description><![CDATA[Musings on model alignment, what determines safety, and where we go from here.]]></description><link>https://www.interconnects.ai/p/lessons-from-the-hacks</link><guid isPermaLink="false">https://www.interconnects.ai/p/lessons-from-the-hacks</guid><dc:creator><![CDATA[Nathan Lambert]]></dc:creator><pubDate>Sun, 09 Aug 2026 14:57:11 GMT</pubDate><content:encoded><![CDATA[<p><span>The recent run of cyberattacks by in-development frontier models has got me thinking a lot about how our current incentive systems are not well suited for such fast technological transitions.</span></p><div><hr></div><h4>Book sale</h4><p><span>I&#8217;m also hosting a book launch where you can get a free signed copy tomorrow from 5-8PM in Seattle (Fremont/Ballard area) &#8211; we still have some extra space so I&#8217;m opening signups to paid subscribers below the paywall:</span></p>
<p>
<a href="https://www.interconnects.ai/p/lessons-from-the-hacks">
Read more
</a>
</p>
]]></content:encoded></item>
<!-- Free post (audience "everyone") that mentions paid subscribers and ends with an embedded post's "Read more". -->
<item><title><![CDATA[When will average people feel AI’s impact?]]></title><description><![CDATA[We&#8217;re <5 years into a compounding revolution which could take a century, and how the AI industry should manage this.]]></description><link>https://www.interconnects.ai/p/when-will-average-people-feel-ais</link><guid isPermaLink="false">https://www.interconnects.ai/p/when-will-average-people-feel-ais</guid><dc:creator><![CDATA[Nathan Lambert]]></dc:creator><pubDate>Wed, 09 Sep 2026 11:01:23 GMT</pubDate><content:encoded><![CDATA[<h5>Housekeeping: Paid subscribers to Interconnects now get a permanent 40% discount on my book when purchasing at Manning.com. Access the code at the Interconnects <a href="https://www.interconnects.ai/perks">perks page</a>.</h5><p>Many AI optimists tend to compare what is happening in this AI boom to the industrial revolution, or to other periods of rapid technological advancement and diffusion into society.</p><div><a class="embedded-post" native="true" href="https://www.fabricatedknowledge.com/p/mythos-and-engels-pause?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web&amp;embedding_publication_id=48206"><div class="embedded-post-header"><span class="embedded-post-publication-name">Fabricated Knowledge</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title">Engels' Pause and the Permanent Underclass </div></div><div class="embedded-post-cta-wrapper"><span class="embedded-post-cta">Read more</span></div><div class="embedded-post-meta">6 months ago &#183; 214 likes &#183; 5 comments &#183; Doug O'Laughlin</div></a></div>]]></content:encoded></item>
</channel></rss>
+17
View File
@@ -2,10 +2,14 @@
import 'dart:io';
import 'package:daily_tech_news/data/default_sources.dart';
import 'package:daily_tech_news/models/news_source.dart';
import 'package:daily_tech_news/services/article_extractor.dart';
import 'package:daily_tech_news/services/consent_service.dart';
import 'package:daily_tech_news/services/feed_service.dart';
import 'package:daily_tech_news/services/storage.dart';
import 'package:daily_tech_news/state/app_state.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
final live = Platform.environment['LIVE_FEEDS'] == '1';
@@ -29,6 +33,19 @@ void main() {
expect(failures, isEmpty);
}, skip: live ? false : 'set LIVE_FEEDS=1 to run', timeout: const Timeout(Duration(minutes: 3)));
test('a full refresh with all on-by-default Sources stays fast', () async {
SharedPreferences.setMockInitialValues({});
final state = AppState(await Storage.open(), feedService: FeedService());
final clock = Stopwatch()..start();
await state.init();
clock.stop();
// ignore: avoid_print
print('${state.enabledSources.length} Sources, ${state.articles.length} articles '
'(${state.articlesOf(SourceKind.learning).length} Learning) in '
'${clock.elapsedMilliseconds} ms; errors: ${state.feedErrors}');
expect(clock.elapsed, lessThan(const Duration(seconds: 15)));
}, skip: live ? false : 'set LIVE_FEEDS=1 to run', timeout: const Timeout(Duration(minutes: 1)));
test('reader view extracts the article body', () async {
final service = FeedService();
final extractor = ArticleExtractor(consent: ConsentService());
+75
View File
@@ -0,0 +1,75 @@
import 'dart:io';
import 'package:daily_tech_news/models/article.dart';
import 'package:daily_tech_news/models/news_source.dart';
import 'package:daily_tech_news/services/feed_parser.dart';
import 'package:daily_tech_news/services/feed_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'support/fake_app.dart';
/// Serves saved Substack feeds (trimmed real items) instead of the network,
/// dated an hour ago so every time window keeps them.
class _SavedFeeds extends FeedService {
_SavedFeeds(this.fixtureByFeedUrl);
final Map<String, String> fixtureByFeedUrl;
@override
Future<List<Article>> fetchSource(NewsSource source, {String corsProxy = ''}) async {
final xml = File(fixtureByFeedUrl[source.feedUrl]!).readAsStringSync();
final anHourAgo = DateTime.now().toUtc().subtract(const Duration(hours: 1));
return [
for (final a in FeedParser.parse(xml, source))
Article(
title: a.title,
link: a.link,
source: a.source,
published: anHourAgo,
excerpt: a.excerpt,
contentHtml: a.contentHtml,
),
];
}
}
List<String> _titles(List<Article> articles) => articles.map((a) => a.title).toList();
void main() {
final interconnects = testSource('interconnects', SourceKind.learning);
final substackNews = testSource('substack-news', SourceKind.news);
setUp(() => SharedPreferences.setMockInitialValues({}));
_SavedFeeds savedFeeds() => _SavedFeeds({
interconnects.feedUrl: 'test/fixtures/interconnects_feed.xml',
substackNews.feedUrl: 'test/fixtures/ahead_of_ai_feed.xml',
});
test('paid newsletter posts are hidden in the Learn tab and Today and counted as hidden',
() async {
final state = await launchApp([interconnects, substackNews], feedService: savedFeeds());
expect(_titles(state.articlesOf(SourceKind.learning)),
['When will average people feel AI’s impact?']);
expect(state.hiddenCountOf(SourceKind.learning), 1);
expect(_titles(state.articlesOf(SourceKind.news)), [
'Controlling Reasoning Effort in LLMs',
'GPT-6 Astra, Looped Transformers, and Hidden Reasoning',
]);
expect(state.hiddenCountOf(SourceKind.news), 1);
});
test('with "hide paid articles" off, paid newsletter posts show as before', () async {
final state = await launchApp([interconnects, substackNews], feedService: savedFeeds());
await state.updateSettings(state.settings.copyWith(hidePaid: false));
expect(_titles(state.articlesOf(SourceKind.learning)).toSet(),
{'Lessons from the hacks', 'When will average people feel AI’s impact?'});
expect(state.hiddenCountOf(SourceKind.learning), 0);
expect(_titles(state.articlesOf(SourceKind.news)), contains('Building an AI Text Detector From Scratch'));
expect(state.hiddenCountOf(SourceKind.news), 0);
});
}
+18
View File
@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:daily_tech_news/models/article.dart';
import 'package:daily_tech_news/models/news_source.dart';
import 'package:daily_tech_news/services/feed_parser.dart';
import 'package:daily_tech_news/services/paywall_service.dart';
import 'package:daily_tech_news/services/storage.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -38,6 +41,21 @@ void main() {
expect(paywall.isSponsored(_article('Anzeigenmarkt bricht ein')), isFalse);
});
test('recognizes paid Substack posts by the cut-off content in the feed', () async {
final paywall = PaywallService(await _storage(), client: MockClient((_) async => http.Response('', 500)));
final posts = {
for (final a in FeedParser.parse(
File('test/fixtures/ahead_of_ai_feed.xml').readAsStringSync(), _source()))
a.title: a,
};
expect(paywall.isPaid(posts['Building an AI Text Detector From Scratch']!), isTrue);
expect(paywall.isPaid(posts['Controlling Reasoning Effort in LLMs']!), isFalse,
reason: 'a free post asking readers to become paid subscribers');
expect(paywall.isPaid(posts['GPT-6 Astra, Looped Transformers, and Hidden Reasoning']!), isFalse,
reason: 'a free post ending with the subscribe widget');
});
test('checks pages only for flagged sources and remembers results', () async {
final storage = await _storage();
final requested = <String>[];
+20
View File
@@ -135,4 +135,24 @@ void main() {
expect(bytebytego.language, SourceLanguage.en);
expect(bytebytego.enabledByDefault, isTrue);
});
test('a fresh install shows posts of the last 14 days from the curated Learning sources',
() async {
final learning = defaultSources.where((s) => s.kind == SourceKind.learning);
final state = await launchApp(defaultSources, feeds: {
for (final s in learning)
s.feedUrl: [('${s.name}: recent', const Duration(days: 13)),
('${s.name}: old', const Duration(days: 15))],
});
expect(_titles(state.articlesOf(SourceKind.learning)).toSet(), {
'ByteByteGo: recent', 'System Design One: recent',
'The Pragmatic Engineer (blog): recent', 'Martin Fowler: recent', 'Julia Evans: recent',
'Dan Luu: recent', 'The Cloudflare Blog: recent', 'Netflix Technology Blog: recent',
'Engineering at Meta: recent', 'GitHub Blog: Engineering: recent', 'Latent Space: recent',
'Import AI: recent', 'Interconnects: recent', 'Ahead of AI: recent',
'Lilian Weng: recent',
// InfoQ is off by default.
});
});
}
+10 -2
View File
@@ -23,6 +23,13 @@ NewsSource _source(String id, {bool onByDefault = false}) => NewsSource(
enabledByDefault: onByDefault,
);
/// The curated Learning sources that are on by default (all but InfoQ).
const _learningOnByDefault = {
'bytebytego', 'systemdesignone', 'pragmaticengineer-blog', 'martinfowler', 'juliaevans',
'danluu', 'cloudflare-blog', 'netflix-techblog', 'meta-engineering', 'github-engineering',
'latent-space', 'import-ai', 'interconnects', 'ahead-of-ai', 'lilianweng',
};
/// Starts the app the way a launch does, against whatever is in preferences.
Future<AppState> _launch(List<NewsSource> curatedSources) async {
final state =
@@ -87,7 +94,7 @@ void main() {
expect(state.enabledSourceIds, {
'heise', 'verge', 'mit-tr', 'new-source',
// Added to the catalog later, on by default.
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', 'bytebytego',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', ..._learningOnByDefault,
});
});
@@ -102,8 +109,9 @@ void main() {
expect(state.enabledSourceIds, {
'heise', 'golem', 't3n', 'verge',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', 'bytebytego',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', ..._learningOnByDefault,
});
expect(state.enabledSources.map((s) => s.id), isNot(contains('wired')));
expect(state.enabledSourceIds, isNot(contains('infoq')));
});
}