Folo 移动端 v0.1.5 版本特性技术解读:通知与未读体系、批量已读与体验打磨
导读
本文围绕 Folo(AI RSS Reader)移动端 v0.1.5 变更记录 展开,逐条解读该版本引入的可配置新条目通知、未读角标与详细未读计数、批量将订阅源或分类标记为已读、AI 摘要默认开启等核心改动,并结合 apps/mobile 源码说明其底层实现链路(Firebase Cloud Messaging 推送、expo-notifications 角标、上下文菜单批量操作等)。读完本文,你将能对照当前仓库源码理解这些发布说明背后的功能入口、默认值配置与数据流走向。
一、版本概览:一次围绕“未读治理”的集中交付
v0.1.5 的变更按 changelog 模板 的三段式组织:Shiny new things(新特性)、Improvements(改进) 与 No longer broken(修复)。三部分围绕同一条主线展开:帮助用户更高效地掌握“有新内容 → 有多少未读 → 一键清空 → 点击直达正文”这一完整阅读链路。
新特性清单如下:
- 为新条目提供可配置的通知(#3424)
- 引入未读角标(#3424)
- 将文档链接整合进动作页(7e553db)
- 支持将订阅源或分类一键标记为已读(53b2136)
- 提供显示详细未读计数的选项(#3453)
其中 #3424 从变更记录看同时牵动通知、角标、分享入口隐藏与分享修复等多个改动,可以推断这是一个围绕“推送与分享体系”的横切工作流。
二、可配置的新条目通知:从 FCM 令牌到点击直达正文
2.1 功能描述
v0.1.5 为移动端引入了可配置的新条目通知:当用户订阅的源产生新条目时,App 可推送提醒,且用户能够按需开关该能力。这说明“是否接收推送”已成为一个可配置项,而不再是无条件行为。
2.2 源码侧的推送链路
在 useMessaging.ts 中可以看到完整链路:
- 令牌注册:
useUpdateMessagingToken在用户登录(whoami?.id存在)且具备通知操作权限(hasNotificationActions)时触发,通过followClient.api.messaging.createToken({ token, channel: Platform.OS })将设备 FCM 令牌上报服务端,同时缓存在本地kv(键名为firebase_messaging_token,见 useMessaging.ts)。 - 令牌刷新:通过 Firebase Messaging 的
onTokenRefresh监听令牌轮换并自动重新注册。 - 消息路由:
useMessaging接收 Remote Message,识别message.data.type === "new-entry"类型的推送,读取其中的entryId与view字段,然后调用navigation.pushControllerView(EntryDetailScreen, ...)直接将用户带入对应条目详情页(见 useMessaging.ts)。
这一设计意味着服务端推送的 data 载荷至少包含 type、view、entryId 三个字段,移动端据此完成“点开推送即读文章”的深度链接体验。
从依赖清单看,推送能力由 @react-native-firebase/messaging(FCM)与 expo-notifications(通知呈现、权限与角标)共同承担,二者均声明于 apps/mobile/package.json。
2.3 权限请求与系统通道
[v0.1.5 之后仍沿用的] 通知权限申请逻辑位于 permission.ts:
- Android 端首先创建名为
default的通知渠道,配置importance: AndroidImportance.MAX、振动模式[0, 250, 250, 250]与提示色; - 随后按“先查询、再请求”的顺序获取权限,未授权时弹出系统请求;
- 权限被拒绝时返回
false并弹出错误提示,不会静默失败。
2.4 可配置性入口
仓库中 Notifications 设置路由 已为通知设置页预留了占位实现,同时 SettingsList.tsx 以 titles.notifications 作为菜单项文案并配 NotificationCuteReIcon 图标,从源码结构看通知配置入口已接入设置体系。
三、未读角标与详细未读计数:一个开关,两种粒度
3.1 未读角标(App Icon Badge)
角标能力由 useUnreadCountBadge.ts 实现:它通过 useUnreadAll() 读取全局未读总数,在每次变化后调用 setBadgeCountAsyncWithPermission(unread) 把数字同步到系统桌面角标(expo-notifications 的 setBadgeCountAsync)。
关键约束在其开关语义上:角标默认关闭。在共享设置默认值 defaults.ts 中 showUnreadCountBadgeMobile: false,对应 UI 设置接口 interface.ts。开关判定实现在 permission.ts:
- 读取
getUISettings().showUnreadCountBadgeMobile; - 开关关闭(默认状态)且未显式跳过检查时直接返回
false,不写角标; - 开启后还需完成通知授权,否则同样放弃;
- 为避免无效调用,若目标计数与当前角标相同则直接返回(幂等保护)。
3.2 详细未读计数(#3453)
除“角标显示/隐藏”这一粗粒度选项外,#3453 提供了显示详细未读计数的选项。从订阅列表组件(如 SubscriptionItem.tsx、InboxItem.tsx)与 CategoryGrouped.tsx 对未读计数的使用可以推断:该选项控制列表内未读数字的展示粒度(例如分组/分类维度上的统计明细),使得大列表场景下用户无需进入具体分组即可感知各维度积压量。
四、批量已读:订阅源与分类的“一键清零”
4.1 功能描述
v0.1.5 让用户可以对单个订阅源或整个分类执行“标记为已读”操作,解决订阅量大时逐条划掉的痛点。
4.2 上下文菜单的实现
在 feeds.tsx 的订阅源上下文菜单中可以看到 markFeedAsRead 的调用:菜单项触发后执行 unreadSyncService.markFeedAsRead(feedIds)(见 feeds.tsx),其中 feedIds 可来自单选订阅源或整组订阅源集合。分类场景则通过对选中分类展开为 feedIds 数组后复用同一 markFeedAsRead 服务完成批量清零,达到“分类维度一键已读”的效果。
4.3 与既有已读能力的配合
该能力与移动端既有“条目级已读”逻辑(apps/mobile/src/modules/entry-list/hooks.ts 等)互补:条目级用于逐条消费,源/分类级用于整体清空,二者共同构成完整的未读治理闭环。
五、动作页文档链接与分享链路收口(#3424 / 7e553db)
5.1 文档链接进入动作页
7e553db 将“文档链接”接入动作页(Actions),对应设置路由 Actions.tsx。从变更语义推断,用户可在条目/订阅的动作面板中直接跳转查看对应的原文文档,减少“复制链接 → 浏览器打开”的手动步骤。
5.2 分享按钮的可用性判断(#3424)
Improvements 中“在不适用于某订阅源时隐藏分享按钮”与 No longer broken 中“修复无法分享订阅源 URL”同属 #3424 的分享链路整改:
- 旧行为:分享按钮无条件出现,但对某些订阅源(例如无站点的纯列表源或特殊订阅类型)分享操作会失败;
- 新行为:先判断该订阅源是否存在可分享的 URL/资源,再决定按钮显隐;同时修复了此前导致分享动作失败的根因,保证对具备分享条件的源能稳定完成分享。
这两条改动在 feeds.tsx 的上下文菜单中也有对应:仅当 subscription.feedId 对应源存在(getFeedById 可命中)时才执行 setStringAsync(feed.url) 之类分享逻辑。
六、AI 摘要与阅读视觉层的打磨
6.1 AI 摘要默认开启(8da8a71)
v0.1.5 将 AI 摘要设置为默认启用。这一语义在共享 UI 设置的默认值中得到印证:defaults.ts 中 summary: true,对应接口 interface.ts 的布尔字段。默认开启意味着新用户开箱即可看到条目摘要,无需手动进入设置开启;该默认值位于 @follow/shared 共享包,可推断为保证桌面端与移动端行为一致。
6.2 AI 摘要样式增强(87aa062)
同版本对 AI 摘要的视觉样式做了增强,使其在正文中与普通内容区分更清晰。移动端 AI 摘要相关 UI 集中在 modules/ai/summary.tsx,可结合该文件继续查看样式实现。
6.3 其余视觉与反馈改进
- 订阅源请求失败的错误提示(#3455):当订阅源抓取失败时不再静默,而是向用户展示明确错误信息。上下文菜单中
feed.errorAt/feed.errorMessage字段的存在(见 feeds.tsx)说明数据层已具备错误状态字段,前端据此渲染提示。 - 时间线列表分隔线样式(b1c356f) 与 Toast 样式升级(83e7764):属于信息密度与反馈层的一致性打磨。
- 显著性能改进(23eaf86):变更记录仅给出哈希而未量化具体指标,从措辞看属于渲染/滚动链路的优化,具体收益以实际使用为准。
七、No longer broken:六个关键修复的技术要点
| 修复点 | 影响 |
|---|---|
| 订阅源 URL 分享失效(#3424) | 恢复对可分享订阅源的分享能力 |
| 同名分类在不同视图下的区分错误(d70f082) | 订阅列表按视图准确区分同名分类,避免“标记已读/展开”作用于错误目标 |
| 重复菜单项(f24f21a) | 清理上下文菜单与动作面板中的重复操作项 |
| 条目文字偶发错位(17dd1e7) | 修正条目文本定位,保证正文排版稳定 |
| 视图选择器按压缩放闪烁(d66d49c) | 消除 Timeline 视图切换时的闪烁,提升交互顺滑度 |
| 特定图片加载导致崩溃(7b20633) | 修复特定图片来源下的稳定性问题 |
其中“同名分类跨视图混淆”的修复对多视图(如时间线/收件箱/订阅列表等视图体系,见 TimelineViewSelector.tsx)用户尤为重要:分类名称相同但归属视图不同时,此前操作可能命中错误上下文,修复后按视图隔离语义得到保证。
八、如何在本仓库验证这些能力
若希望对照源码逐条验证 v0.1.5 的能力,推荐按以下路径阅读:
- 推送与角标:useMessaging.ts、permission.ts、useUnreadCountBadge.ts,配合共享默认值 defaults.ts;
- 批量已读:context-menu/feeds.tsx;
- 设置入口:modules/settings/SettingsList.tsx 与
modules/settings/routes/下的 Actions、Notifications、General 路由; - 依赖版本:通知与角标相关的
expo-notifications、@react-native-firebase/messaging等声明见 apps/mobile/package.json; - 相邻版本对比:可对比 v0.1.4 与 v0.1.6 的变更记录,观察该特性线的演进脉络。
需要说明的是:changelog 中的版本号(0.1.x)是移动端早期里程碑的记录;以当前仓库为准,apps/mobile/package.json 中包的当前版本已迭代至更高版本,本文所述功能的能力入口与默认值均以仓库内源码现状为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00