Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f035e4bb4 | ||
|
|
b634118ed8 |
No files matched your search
@@ -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
|
||||
@@ -181,6 +184,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
|
||||
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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')));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user