Skip to content

Temp1258/PoopHub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

182 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

拉无忧 · Couple Buzz

一款专为情侣两人设计的亲密互动 App。把日常的小事攒成关系里的仪式感。

Release Tests License Platform Stack

仓库是 monorepo,含两个独立项目:


功能(按 tab 组织)

App 底部 6 个 tab:拍拍 · 废话区 · 每日 · 信箱 · 约定 · 数据

底部导航是自定义的灵动岛风格 PillTabBar:胶囊状按钮、onPressIn 派发瞬切(不等抬手)、上方一段 fade-up 渐变让屏幕内容自然没入工具栏;选中态粉色填充 + spring 弹性放大;4 个 tab(拍拍 / 废话区 / 每日 / 信箱)顶角有未读红点。

🤚 拍拍(Home)

  • 实时摸一摸:双方在线时按住屏幕同步,主页心跳动画 + 持续 haptic
  • 同时在线感知:两人同时打开 App 时主页提示「你们正在同时想着对方 💓」;重连后服务端会主动下发权威 presence 快照,避免残留状态
  • 顶部状态条:连续天数 🔥 / 置顶纪念日倒数 / 双方在线指示

💬 废话区(History)

  • 50+ 表情一键发:4 类网格(表达爱意 / 心情 / 日常 / 找你),上滑出下滑收
  • APNs 推送 + Haptic:每个动作推送对方手机 + 触感反馈
  • 在线静音:对方在 App 前台时跳过 emoji 推送,红点完全由 socket 驱动,避免双端同时在线时锁屏被刷屏
  • 聊天式时间线 + 连发折叠:按日聚合;5 分钟内同一表情连发屏幕内自动折叠成 表情 ×NN,新一下落地时计数弹跳一次;锁屏推送同步合并 —— server 端 bursts.ts 跟踪 5 min 滚动窗口,第二条起带 ×N 文案 + 稳定 collapseId,APNs collapse-id 把旧通知替换掉,5 个 💋 spam 在锁屏只显示 1 条 banner,数字滚到 ×5(v1.3.1)
  • 上拉翻页:列表顶端再拖动一次触发 loadOlder(before_id cursor + has_more),SectionList 用 maintainVisibleContentPosition 让 prepend 旧消息时可视区不跳;单次拖动只触发一次,要再加载下一页必须重新拖动(v1.3.2)
  • 未读分界线 + 水滴入场:上次离开位置自动画一条「以下是新消息」分界线,新消息以水滴 spring 入场动画落入列表
  • 长按标题自定义:长按页面标题(默认「香宝聚集地 💕」)弹 prompt 改文案,30 字上限、whitespace-only 视为清空,按账号 per-user 持久到服务端(v1.2.20)
  • 时区感知:每条记录按双方各自所在时区分别显示时间
  • 桌面 badge 真实未读数:iOS 图标右上角显示对方发的未读消息数(一次性 mark-read 防客户端越权)

📅 每日(Daily)

  • 早安/晚安打卡:按本地时区窗口(早 4-13 点 / 晚 18-4 点)开放;双方都打卡后展示当日互动 recap
  • 每日问答:1000+ 题题库,双方都作答后才互相揭晓;按北京时间 07:00滚动新题,距下次刷新倒计时合并到屏幕底部
    • 一次性 👍 / 👎 互评 + 评价后推送通知对方
    • 自己已答对方未答时显示「⏰ 快答!」催答按钮(30s cooldown)
  • 每日快照:每天一张前置自拍,按月日历查看
    • 同样支持一次性 👍 / 👎 互评 + 「⏰ 拍照!」催拍按钮(5s cooldown)
    • 上传 atomic(写 tmp → 校验 → rename),防绕过 client 覆盖已有照片

📮 信箱(Mailbox)

v1.1 重构:原本平铺的 MailboxCard / TimeCapsuleCard 全部下沉,主屏只剩三张入口卡 + 底部「写信 ✉️」灵动岛 pill。点入口卡以全屏 pageSheet 模态弹出对应子界面。这套结构让信箱真正像一个「桌子」:写信入口永远停在底部、收件 / 整理 / 留言三件事各回各家。

主屏入口(自带未读 🚩 红旗):

  • 📬 收件箱 — 已送达的半日达 + 已开启的择日达
  • 🗑️ 废件箱 — 从收件箱删除的信件可以在这里恢复(v1.1 前叫「垃圾篓」)
  • 📝 小贴吧 — 双方共享的便利贴墙(v1.1 新增)
  • 📷 快照日历 — 按月查看双方每日自拍:7 列网格 + Polaroid 缩略图 + 月份切换;两人都拍当天双张错位叠放(我的在上 +2° / ta 的在下 -2°),today cell 粉边框;右下角「ta / 我」segmented toggle 单视角切换;点 cell 全屏预览,点空白处收起(v1.2.21 / v1.3.0–v1.3.2)

底部固定的 ✉️ pill 进入统一写信流程(半日达 / 择日达分支由封信后选择)。底部还有 📤 发件箱入口(OutboxScreen),列出自己已寄出但还没送达的半日达 / 择日达,按上下滑动浏览,已送达自动从列表里消失;新寄出的灵动岛 toast + 信箱 tab 红点立刻点亮,无需等推送回环。

✉️ 统一写信流程(WriteLetterScreen)

5 个阶段write → sealing → kind → capsuleDetails → sending

  • :奶油色信纸(#FAF6E8)+ 棕墨字(#3D2A19);正式信件版式:致 [对方/自己] · 正文 · 落款 [自己] · 字数计数;iOS 键盘上方 inline accessory 「完成」按钮一键收键盘;草稿 400ms debounce 自动落 AsyncStorage,关掉重开还在
  • :SealAnimation(信纸 → 信封 → 火漆印)~1.3s,作者本人也看不到自己写的内容(my_sealed 服务端标志,writing/sealing 阶段不返回 my_message;客户端草稿 setter 在 sealing 之后失活,避免 stale closure 把 sealing 阶段的 UI 草稿写入)
  • :📮 半日达 / 💌 择日达,二选一卡片;两类信件 1000 字上限对齐(半日达 v1.3.7 从 500 提到 1000,前端字数计数与服务端 reject 阈值统一);半日达单场可多发(同一窗口写多少封都行,到点一起送达),不再是「一场一封」的限制
  • 择日达详情:年/月/日 + 时/分五段下拉选择器(不再是日历点击,6 年窗口,月日联动 clamp);双时区即时预览——同时显示「我(北京时间):04-27 20:34」与「ta 那边收到时:04-28 04:34」,让对方拿到的也是整点钟整分;可见性切换 🪞 给自己 / 💕 给对方
  • :信件缩小 + 微微旋转 + 沿 Y 轴落入信箱图标(~520ms)+ 信箱小弹跳(~180ms),左右随机偏置增加变化

📬 收件箱(InboxScreen — Apple Wallet 风格)

  • 中央卡居中 snap,scale 1 / 邻居 0.93 / 远端 0.86 体现景深,所有卡保持 opacity 1 全部展示;卡片层叠 75% / 露出 25%
  • 中央卡 tap 直接快速预览(fade + scale up,~250ms);邻居 tap 自动滚到居中
  • 滑动 haptic + 标题栏渐变 + 未读 pill + 邮戳时间戳(双时区邮戳 = 寄出方 TZ + 收件方 TZ)
  • 右划删除:仅中央卡可触发,飞出阈值 38% 屏宽,触发后调用 trash API + 顶部弹出灵动岛风格 toast
  • 未读小旗子:服务端 letter list 与本地 INBOX_LAST_SEEN cursor 比对,新到的飞旗子;打开 InboxScreen 即刷新 cursor 到 now
  • 长信完整可读:信纸用「definite-height 根 + flex 链」布局,header / footer / Dynamic Type 怎么变 ScrollView 都拿得到正确剩余空间;底部「还有更多」渐隐条 + scrollIndicator + 「拖动阅读 · 轻点空白处收起」提示文案三处 affordance 让用户知道可以滚(v1.3.4 / v1.3.6)
  • 长信不再牵动收件箱:letter overlay 包在自己的 transparent Modal(=iOS overFullScreen)里,盖住底下的 pageSheet InboxScreen,pageSheet 的 swipe-to-dismiss recognizer 收不到 touch,长信 ScrollView 滑到边界只是自己 bounce 回弹,不会把整个信箱 modal 拖走(v1.3.8)
  • 排序保障(arrivedAt ASC, writtenAt ASC) 复合排序 + scroll-to-bottom 初始化,同 session 多封信按写信时间 tiebreak,新解锁的 capsule 也能落到顶层(v1.2.17 / v1.2.18)

🗑️ 废件箱(TrashScreen)

  • 单条「恢复」/「彻底删除」+ 选择模式批量「全部恢复」/「全部删除」
  • 「彻底删除(purge)」服务端永久隐藏:archive、capsules、open endpoint 三处统一拦截,无法恢复
  • 主信箱页下拉刷新一并刷新收件箱 + 废件箱

📝 小贴吧(StickyWallScreen — v1.1 新增)

双方共享的木质便利贴墙。把那种「想发又不值得发一条单独消息」的小事写出来,贴一张,过几天对方贴一张回应——比聊天更轻、比朋友圈更近。

  • 木色背景 #D4B68C + 奶油便利贴 #FFFBE6;自己的字粉色 #A0144A,对方的字蓝色 #0F4F8A
  • 掉落入场动画:scale 1.35→1 + translateY -32→0 + opacity 0→1,spring
  • 跟帖 = 订书钉串联的便利贴堆:每个跟帖一张独立纸,纵向重叠 14pt + 圆钉装饰(pin head + highlight 高光)+ 纸面随机 ±1°-±5° 微旋转,新纸在最上层
  • 双击便利贴 → 跟个帖:300ms 内同 sticky 二次轻点直接弹出回复编辑器
  • 撕下来过场动画 + 永久删除:长按 → 二次确认 → scale 缩 + 随机 ±22° 翻转 + Y 轴上飘 + 淡出(~320ms)→ 调 DELETE 接口级联清掉所有 block 和 seen 记录
  • 草稿持久化(temp):每人每对最多一张未发布的 sticky;编辑器 1200ms debounce 自动保存到服务端 status='temp',关掉再开还在;类似地,跟帖也有 temp 阶段
  • 背景点击收起 + 底部「收起」pill + 标题贴顶;点击空白区域收回编辑器,连点保留草稿
  • 未读旗子:每张 sticky 维护 last_seen_block_id,对方有新 block 则首张纸右上角亮一颗「未读」灵动岛
  • socket 实时推送:服务端 sticky_posted / sticky_appended APNs 推送 + sticky_update socket 广播;信箱 tab 红点和入口卡红旗实时刷新

🎀 约定(Anniversary & Wishes)

所有「过去的约定」和「未来的约定」集中地。

  • 多纪念日管理:增删改、置顶倒数(首页显示)、支持每年重复
    • 年/月/日三段下拉选择(不再用日历点击,方便选 20 年前的纪念日)
    • 已过去的不重复纪念日自动显示「已经 N 天啦!」
    • 「添加纪念日」改用 App 级灵动岛 pill 入口
    • 每条纪念日带「编辑」按钮,复用同一 picker 表单,提交分支 POST vs PUT 切换;以前只有「删了重建」一条路(v1.3.7)
  • 心愿清单:一起完成的 bucket list,按分类管理(旅行 / 美食 / 活动 / 其他)
    • 「添加心愿」也是灵动岛 pill;输入框不自动 focus,避免一进页面就弹键盘
    • 创建者区分:每条心愿左侧 4px 彩条 + 右侧 chip,自己粉色(COLORS.kiss)/ 对方浅蓝(#7AB8D6
    • 完成时二次确认 + 屏幕烟花动画庆祝 + 推送带具体心愿名给对方

📊 数据(Stats / Settings)

  • 双方 ID 卡片:左右并列展示
  • 互动统计:总互动数、双方对比、最爱表情 Top 5、按月趋势
  • 恋爱周报:本周互动量、与上周对比、连续天数、温度评分(互动 + 问答 + 打卡 + 连续天数四项加权)
  • 昵称 + 备注 配对行:双方昵称、彼此私密备注左右镜像并排,编辑后居中弹出灵动岛保存 pill
  • 昵称 + 时区设置:时区像 ID 一样并列显示,点选即自动保存
  • 多设备列表(DeviceListCard):列出当前账号所有登录中的设备(设备名 / 机型 / 系统 / App 版本 / 上次活跃时间);任一为「主设备」,本机标注「本机」徽章;可一键设主设备 / 强制下线某台 / 自我下线;强制下线即时生效(被踢端下次请求 401,access token 也会被 token_version 拒绝),不用等 token 自然过期
  • 解除绑定:双侧均可发起;解绑后双方所有 couple-scoped 数据(History / 信箱 / 小贴吧 / 心愿 / 纪念日 / 每日问答…)保留 90 天,期间重新绑定整套数据自动复活,过期才硬删

系统能力

  • iOS WidgetKit 小组件:桌面卡片 Swift 框架已搭好(couple-buzz-app/targets/widget/,预留 partnerLastEmoji / partnerLastActionTime / streak / partnerName 4 个 App Group UserDefaults key)
  • OTA 热更新:纯 JS 改动通过 EAS Updates 秒推到手机,无需重 build
  • JWT 双 token + 多设备会话:access 15 分钟 / refresh 90 天,refresh token 自动轮换 + 单事务化轮换 + 并发刷新加锁;每条 refresh token 绑定一个 session_id(设备名 / 机型 / 系统 / App 版本 / 上次活跃 / is_primary / revoked 都在 refresh_tokens 行内),auth 中间件每次请求都过 isSessionActive(),强制下线 / 主设备切换即时生效;token_version 字段保留作全账号即时吊销开关
  • 弱网 refresh grace 5 min:rotation grace window 从 10 s 拉到 300 s 后地铁 / 电梯 / 弱 Wi-Fi 上 refresh 响应丢包不会被踢;客户端 bootstrap 遇 generic AuthError 等 2.5s 再 retry 一次 getStatus 才决定清账号;revoked session 仍立即返回 code:session_revoked 强制下线,安全语义不变(v1.2.19)
  • 重登多设备智能合并:以 APNs device_token 作「同一物理设备」唯一可信信号 —— /loginPUT /device-token 检测到 token 已绑定到本用户旧 session 时自动 revoke 那个旧 session 并把 device_name + is_primary 继承过来;两台同名 iPhone(APNs token 不同)依旧并存。客户端登录带上 storage.getApnsTokenCache() 缓存值(device-scoped, 不参与 clearAll)(v1.2.17)
  • 多设备 APNs 推送device_tokens 表(apns_token PK + user_id + session_id)替代 users.device_token 单值字段,一个用户在 N 台设备登录就 N 行;pushToUser 自动 fan-out 到所有设备,APNs 410 失效 token 自动清理;与 sessions 关联的 token 在 DELETE /sessions/:sid 时一并删
  • 跨端状态同步上服务端:每日已读(daily_seen_date / pa / ps)/ 收件箱已读(inbox_last_seen)/ 发件箱已读(outbox_last_seen)/ 写信草稿(write_letter_draft)以前都只在 AsyncStorage,换设备 / 重装就丢;现在全部存到 users 表对应字段,多设备状态实时一致;写入路径加 only-advance 守卫,旧请求乱序到达不会把游标 roll back
  • pair_id 关系实体 + 90 天数据 TTLcouples 表把每段「关系」抽象成稳定 10 字符 pair_id(用户 a < b lex 排序保证唯一),所有 couple-scoped 数据(actions / mailbox / capsules / sticky / important_dates / daily_answers / ritual / inbox_actions)都打 pair_id 标签;解绑写 ended_at90 天内重绑同一对人 → ended_at 清空,全套数据复活;TTL 调度器 03:00 UTC 每日扫一次过期 couple 硬删
  • APNs badge 全推送覆盖 + 每推送 +1pushToUser 在 caller 没传 badge 时兜底取真实跨功能未读数(History + 信箱 + 已解锁 capsule + 小贴吧 partner block 累加,floor=1);scheduler.broadcastPush 也按用户 fan-out 走同一管道;并额外维护 users.unack_push_count 计数列,每次 push 自增 1,最终 badge = max(1, 跨功能未读数, 该计数),让 date_new / ritual_* / weather_* / urge_* / weekly_report / bucket_* / snap_* 这些没有自己未读游标的推送类型也能让图标数字 +1;前台 POST /api/badge-ack reset 计数(v1.2.6 起累计修复,v1.2.18 起 +1 计数)
  • 限流分层:注册 / 配对 / 认证 / 普通 API 各自独立的速率限制
  • Socket 触摸限速:服务端 5 events / 1s 的滚动窗口拦截畸形/恶意客户端,省电省 APNs 配额
  • 多设备并发 socket:presence 用 Map<userId, Set<socketId>> 维护,手机 + iPad 同时在线不再误报对方掉线,touch_end 仅在最后一个 touching 设备离开时广播
  • Presence 防闪 / 防残留:3s debounce 才广播 presence_both 避免快速重连闪烁;disconnect 留 1.5s grace + stale-closure guard(旧 closure 检测到 presence 已被新 session 覆盖直接 bail);on-connect 给孤身 socket 主动补一次 presence_single 治愈历史残留状态
  • Capsule 解锁推送防重复 + 分钟级精度time_capsules.notified_at 列 + 调度器分钟级 dedup key,进程重启 / 时钟漂移 / 多次扫描都不会让对方收到第二遍开信通知;scheduler 去掉 5 min 节流改成每分钟扫一次(v1.2.17);GET /api/capsulesauto-open sweep,到点的 capsule 在 recipient 拉列表时直接 opened_at=unlock_at,对齐半日达「session reveal 自动揭晓」语义,且避免 race 把刚自动 open 的 capsule 再次标成新到达
  • socket daily_update 转发:v1.3.7 修复 services/socket.ts 漏接 daily_update 的 bug —— 之前 DailyQuestionCard / DailySnapCard 的 subscribe('daily_update') 永远收不到,要手动下拉刷新才能看到对方答题 / 拍照;现在双方同在「每日」tab 时一方提交对方屏幕立刻刷新
  • 响应式适配:爱心心跳 / 触摸圆环 / 写信 pill / 卡片高度 / 信纸字号 / iPad 信件宽度等 7 处按屏幕尺寸动态计算,从 iPhone SE 到 iPad mini 都不变形
  • 健康检查GET /health
  • 下拉刷新:每日 / 信箱 / 数据三 tab 都支持
  • 加密备份:每日 03:00 GPG 公钥加密 SQLite,私钥离线保管,详见 couple-buzz-server/docs/BACKUP.md
  • 灵动岛风格 in-app toast:屏幕顶部胶囊形提示,slide + spring 进出(IslandToast.tsx
  • 顶部 scroll-bound fade:每日 / 信箱 / 约定 / 数据 4 屏列表顶部按 scrollY 渐隐,与底部 PillTabBar 形成对称的内容淡入效果
  • App 级 Toolbar SlotToolbarSlotContext 把屏幕的「写信 / 添加纪念日 / 保存备注」灵动岛 pill 提升到 App overlay 层,不会被屏幕内的渐变遮罩压住

推送与导航

  • deep-link 路由react-navigation linking 配置 + couplebuzz:// scheme,cold-launch 用 getInitialURL(),warm-tap 用 addNotificationResponseReceivedListener 订阅;点击通知后稳定跳到对应 tab,告别手写 nav-queue
  • iOS 锁屏合并:相同对话方向的通知合并展示,避免连续摸一摸刷屏
  • APNs payload 兼容:服务端把所有自定义字段包在 body 顶级 key 下,解决 expo-notifications 从 userInfo['body'] 取数据的兼容性问题
  • 桌面图标 badge 全覆盖:以前只有废话区表情 / 反应 / 摸一摸三种推送会带 aps.badge,sticky / 信箱 / 每日 / 快照 / 心愿 / 纪念日 / 仪式 / capsule / 催答催拍 / 解除配对 / 周报 / 天气等 ~25 种推送都把字段省掉,APNs 规范是「缺 badge → 不动图标」,叠加客户端进前台清 0,桌面红圈一直显示不出来;v1.2.6 起 pushToUser 兜底取真实跨功能未读数(floor=1),所有推送类型都会刷新红圈
  • 小贴吧实时联动sticky_posted / sticky_appended 走 APNs,sticky_update 走 socket,信箱 tab 的红点 + 入口卡 🚩 同时刷新;点击通知 deep-link 直接进信箱

技术栈

移动端

  • React Native 0.81 · React 19 · Expo SDK 54 · TypeScript
  • React Navigation v7(material-top-tabs + 自定义 PillTabBar,6 tab 底部导航 + 顶部 swipe)
  • Socket.IO Client(实时触碰 + presence + sticky_update)
  • Expo Notifications / Haptics / ImagePicker / Updates / FileSystem / LinearGradient
  • 内置 Animated API + PanResponder 实现 wallet cascade、信封开合、右划手势、PillTabBar spring、便利贴掉落 / 撕下来、写信寄出动画(未引入 reanimated,所有动画 OTA 可推)
  • @bacons/apple-targets(iOS Widget 原生构建)
  • EAS Build (internal IPA) + EAS Updates (OTA)

服务端

  • Node.js 20 · Express 4 · TypeScript
  • SQLite(better-sqlite3 WAL 模式,启动时自动迁移 schema;表分组:用户 / 关系 couples / 设备 token device_tokens / 互动 actions / 信箱 mailbox + time_capsules + inbox_actions / 每日 daily_answers + daily_snaps + daily_reactions + rituals / 小贴吧 sticky_notes + sticky_blocks + sticky_seen / 心愿 bucket_items / 纪念日 important_dates
  • Socket.IO Server(一次性 ticket 30s TTL 鉴权,多设备 socket Set,重连权威 presence 快照,touch 5/1s 滚动窗口限速)
  • @parse/node-apn(APNs HTTP/2 推送 + 多设备 fan-out + 失效 token 自动清理 + payload 包 body 兼容 expo-notifications + badge 全覆盖兜底)
  • Multer(图片上传 5MB + image MIME 白名单 + atomic tmp/rename 防覆写)
  • JWT + scrypt 密码哈希 + refresh token 哈希存储 + 并发轮换锁 + 事务化轮换 + per-session 设备元信息 + 强制下线即时生效
  • node-cron 调度(mailbox reveal / capsule unlock / 周报 / couple TTL 03:00 UTC 硬删,capsule 推送 notified_at 列持久 dedup)
  • Jest + supertest(15 个 suite / 366 个测试用例 —— 含 api.test + regression_h_bugs / ml_bugs / coverage_gaps + 11 个版本回归 suite v1.2.17 → v1.3.8)
  • express-rate-limit
  • GPG 加密备份 + cron + 离线私钥

安全(持续审计 + 漏洞修复)

维度 实现
密码 scrypt + crypto.timingSafeEqual(防时序攻击)
Token JWT 双 token + token_version 即时吊销 + refresh token 哈希存储 + 自动轮换 + 并发刷新锁 + 单条事务化轮换
多设备会话 per-session is_primary / revoked / device_* 元数据;强制下线立刻吊销该 session(auth 中间件每请求过 isSessionActive);DELETE /sessions/:sid 同时清掉绑定的 device_token;自我下线返回新登录入口
pair_id 关系隔离 couples 表唯一 pair_id(user_a < user_b lex 排序);所有 couple-scoped 数据贴 pair_id 标签;解绑写 ended_at 进入 90 天 grace;中间换过别的对象再换回来不会读到错误关系的旧数据;TTL 后硬删
Cross-pair lookup 防御纵深 getMailboxMessageById / getCapsuleById 签名加 pairId 参数,row 自身 pair_id != null && != ctx.pairId 时直接 undefined;3 处调用点(validateInboxRef×2 + inbox/trash unopened check)全部传 ctx.pairId(v1.3.7)
图片访问 HMAC 签名 URL(1h TTL + timing-safe verify + 路径正则严格)
WebSocket 一次性 ticket 30s TTL + origin 白名单 + 多设备 Set 维护 + touch 5/1s 滚动窗口限速
SQL 注入 全部 prepared statements 参数化绑定,零字符串拼接
输入校验 typeof + length 上限 + YYYY-MM-DD 严格格式 + week/month 范围校验 + daily-reaction 一次性服务端锁 + sticky content 长度校验 + history_title trim/30 字
文件上传 atomic tmp+rename + DB pre-check + 5MB + MIME 白名单
路径穿越 regex 严格 + HMAC + 文件名/路径不取自 client 输入
随机源 crypto.randomInt 生成用户 ID / 配对码
限流 注册 / 配对 / 认证 / API 分层 + socket touch 限速
Badge 计数 服务端真实未读数 + clamp 防客户端越权推进读位指针
Badge 全覆盖 pushToUser 在 caller 不传 badge 时兜底取跨功能未读总数(floor=1),所有推送类型都会刷新桌面图标;scheduler.broadcastPush 也按用户 fan-out 走同一管道,避免直调 sendPush 绕过
跨端状态同步 daily_seen / inbox_seen / outbox_seen / letter_draft 全部上 server 字段,only-advance SQL 守卫拒绝旧客户端 roll back 已读游标
快照反窥探 对方拍照后自己未拍时不下发其照片 URL,避免「先看对方再决定自己拍什么」的偷看;daily_snaps 接口按对称性裁掉敏感字段
时间胶囊 self 可见性服务端校验(防 partner 猜 id 越权读取)
半日达内容封存 writing/sealing 阶段服务端不返回作者自己的 my_message,加 my_sealed 标志;客户端草稿 setter 在 sealing 后失活,避免 stale closure 把 UI 草稿覆写 sealed 内容
Inbox 软删除 inbox_actions 表 per-recipient 状态机(trashed / purged);archive、capsules、open endpoint 三处统一拦截
Purge 防绕过 彻底删除后即使直接调 POST /capsules/:id/open 也返 404,杜绝从历史 id 拿回内容
推送隐私 scheduler 不给 self-vis 胶囊推送 partner,防止泄露用户私密信件存在
Capsule 推送防重 notified_at 列 + scheduler 分钟级 dedup key,重启 / 时钟漂移不会重发开信通知
Sticky 多写竞态 撕贴 vs 跟帖并发:服务端在 commit 前校验 sticky 仍存在,撕贴成功后 cascade 删除 blocks + seen 记录
Presence 残留 disconnect 1.5s grace + stale-closure guard(旧 closure 通过身份 token 比对识别已被新 session 覆盖)+ on-connect 给孤身 socket 补 presence_single
数据备份 GPG 公钥加密(AES-256),私钥离线 U 盘 + 密码管理器 passphrase

v1.3.8(2026-05-27,Latest

长信滑到边界牵动收件箱整体 — 改用 nested Modal 隔离手势

  • 根因:InboxScreen 是 presentationStyle="pageSheet" 原生 Modal,自带 swipe-to-dismiss 识别器。EnvelopeOpenAnimation 之前以 wrapInModal={false} 渲染成 sibling absoluteFill,没法屏蔽 iOS native 那个 pageSheet recognizer,长信 ScrollView 滑到边界把整个 InboxScreen modal 带走
  • 修复:InboxScreen 移除 wrapInModal={false} 回到默认行为 —— letter overlay 包在自己的 <Modal transparent>(=iOS overFullScreen)里,无 swipe-to-dismiss + 完全盖住底下 pageSheet 吞掉所有 touch,ScrollView 自包含,bounce 回弹不会牵动 pageSheet
  • 顺手把 wrapInModal docstring 改成「DO NOT pass false from inside a pageSheet Modal」+ 引用 v1.3.8 根因防下次踩坑
  • regression_v1_3_8.test.ts 6 条 + 全量 15 suites / 366 tests passed

v1.3.7(2026-05-27)

4 项审计修复

  • P0 半日达 501–1000 字写不出去 —— server 上限 500 → 1000 与 capsule 对齐,删掉前端那段已无意义的 client-side 500 字守卫
  • P1 daily_update socket 事件客户端没转发 —— services/socket.ts 加一行 socket.on('daily_update', d => emit(...)),与已有 7 个事件同模式,DailyQuestionCard / DailySnapCard 的 subscribe 终于生效
  • P1 编辑纪念日功能后端有路由前端没调 —— api.updateDate(id, title, date, recurring) + AnniversaryWishScreen 加 editingDate state / handleStartEdit / handleSaveDate / 复用 picker,提交分支 POST vs PUT;每条纪念日加「编辑」按钮
  • P2 Cross-pair lookup 防御纵深 —— MailboxMessage / TimeCapsule interface 加 pair_id(nullable 兼容 legacy);getMailboxMessageById / getCapsuleById 签名加 pairId 参数;3 处调用点全部传 ctx.pairId
  • regression_v1_3_7.test.ts 13 条 + 全量 14 suites / 360 tests passed

v1.3.6(2026-05-27)

长信看不到全貌 — 砍掉 200pt 硬编码 reserve 改 flex 链

  • 根因:contentScroll.maxHeight = LETTER_MAX_H - 200 在 iPhone SE 1st (568pt) / Dynamic Type Large+ / 长备注名换行三种边缘情况下都是错的,ScrollView 视口要么窄到 7-8 行要么直接溢出 letterCenter
  • 修复:布局改成「definite-height 根 + flex 链」—— letterCenter / letterContainer 用 height (definite);letterPress 去掉 minHeight/maxHeight 改 flex:1;contentWrap 加 flex:1 + minHeight:0(RN column-flex shrink 必填);contentScroll 改 flex:1 砍掉 flexShrink + maxHeight
  • 权衡:短信卡片现在固定 LETTER_MAX_H 高度有留白,视觉上更「信件感」且和长信尺寸一致
  • 347 tests passed

v1.3.5(2026-05-27)

fadeOpacity inputRange 双分支避免重复边界

  • overflowAmount 在 (0.5, 30] 时原写法生成 inputRange=[0, 0, X] 有重复首边界,RN 文档说「允许相等但行为 implementation-defined」;按 overflowAmount > 30 分两条等价分支,inputRange 严格递增更耐 RN 版本升级

v1.3.4(2026-05-27)

长信加 3 处滚动 affordance — indicator / 底部渐隐 / 拖动文案

  • v1.3.3 改对了 layout 但用户不知道可以滚 —— 把发现性补齐:
    1. showsVerticalScrollIndicator 重新开启(iOS 触摸期间可见,平时不打扰)
    2. 底部「还有更多」渐隐条(LinearGradient transparent → white),仅 contentHeight > scrollViewHeight 时挂载,Animated 跟 scrollY 联动距底 30pt 内淡出,pointerEvents="none" 不抢手势
    3. tapHint 文案改成「拖动阅读 · 轻点空白处收起」
  • useEffect 在 (visible, content) 切换时重置 scrollY + measurements 防换信带着上一封的 scroll 位置
  • regression_v1_3_4.test.ts +13 / 344 tests passed

v1.3.3(2026-05-27)

长信无法滚动 + 撕下来 500

  • #1 信纸读取无法滚动contentScroll 只有 flexShrink 没显式 maxHeight,RN 0.81 在 flex column 中 ScrollView 撑成 content 高度,父卡片 clip 在 maxHeight 但 ScrollView 内部感知不到 overflow → 不滚。临时改 maxHeight = LETTER_MAX_H - 200 让长信能滚(v1.3.6 进一步改成 flex 链);envelopeWrap 的 pointerEvents nonebox-none 首次解开模式下也能滚
  • #2 撕下来 500deleteSticky() 还在按 v1.2.0 之前 5-placeholder OR 子句传参,但 pair_id 重构后 stmtGetStickyForCouple 只接受 2 个占位符 (id, pair_id),better-sqlite3 抛 RangeError → 500。改函数签名 (stickyId, pairId),路由层传 ctx.pairId
  • regression_v1_3_3.test.ts +12 / 331 tests passed

v1.3.2(2026-05-22)

废话区翻页改成单次拉动触发 + 快照日历 ta|我 toggle

  • HistoryScreen 翻页修复:之前用户拉到顶部 onScroll 持续触发 loadOlder 直到 has_more=false 把对话一路刷到第一条。改成 interaction state machine(idle / dragging / momentum + per-interaction fired latch):每次手指拖动至多触发一次 loadOlder,要下一页必须重新拖动
  • SnapCalendarScreen ta|我 segmented toggle:右下角与「收起」pill 同一行,每格只渲染当前模式那一侧的照片(PolaroidThumb 改成单图 + 底部 accent 条:粉色=我 / 蓝色=ta),预览 overlay 同样过滤;反窥探在服务端不变
  • regression_v1_3_2.test.ts +21 / 319 tests passed

v1.3.1(2026-05-22)

表情连发推送合并 + 废话区上拉翻页 + 快照日历点空白收起

  • #1 同表情连发 push 合并(锁屏只显示 1 条 ×N):新建 server/src/bursts.ts 在内存里跟踪 (recipient, sender, action_type) burst,5 min 滚动窗口跟 v1.2.20 客户端对齐;trackBurst 每次返回当前 count + 稳定的 collapseId(整段 burst 共用)。/api/action 调 trackBurst,count > 1 时构造 "{name} … ×N" 作 bodyOverride,始终带 collapseId;APNs collapse-id 把锁屏的旧通知替换掉,5 个 💋 spam 在锁屏只出 1 条 banner 数字滚到 ×5
  • #2 废话区上拉翻页(loadOlder)stmtGetHistoryBefore SQL(WHERE a.id < ?);GET /api/historybefore_id 查询参数 + has_more 响应字段;客户端 api.getHistory(limit, beforeId?) + HistoryScreen state 重心从 sections 改成 rawActions,sections 用 useMemo 派生,mergeRaw 按 id 去重合并;加 onScroll(距顶 < 80pt 触发)+ onListContentSizeChange(latestId 真变大 + 用户在底部才 scrollToEnd 避免 prepend 跳屏);SectionList 用 maintainVisibleContentPosition 让 prepend 旧消息时可视区不动
  • #3 快照日历点空白收起:ScrollView 套 <Pressable onPress={onClose}>,cell 自己的 TouchableOpacity 优先消费 onPress,scroll 手势归 ScrollView;点 grid 区空白(cell 间隙 / padding / 空月份)收起
  • regression_v1_3_1.test.ts +21 / 298 tests passed

v1.3.0(2026-05-22)

快照反窥探 4 个完整场景测试

  • 补 v1.2.21 验收:服务端反窥探机制现在由 4 个测试从双方视角全覆盖(之前只有 1 个单边测试)—— 1) 只 ta 拍 → 我 my_photo=null & partner_photo=null + ta 自己只看到 my_photo;2) 只我拍 → 镜像 case;3) 双方都拍 → 双方都看见两张(both_snapped=true);4) 跨日不解锁 → 我拍 day-A 不会泄露 ta 的 day-B 照片(per-day check)
  • 277 tests passed

v1.2.21(2026-05-22)

信箱新增快照日历 + 全清「废话区反应」功能 + 死代码清理

  • 反应功能完全移除:服务端 POST /api/reaction 整条路由删 + dbOps.addReaction/getReaction/updateReaction/getHistoryReactions + 对应 SQL prepared statements 全删;/api/history response reactions:{} 空对象保留一发兜底旧 OTA bundle;push.tsreaction 模板;客户端 api.sendReaction / ReactionResponse / ReactionPicker.tsx 整文件删;ActionRecordonLongPress + reactions props;actions.reply_to 列保留(SQLite drop-column 需重建表,老 orphan 行被 reply_to IS NULL filter 隔离)。注意:daily-reaction(每日问答 / 快照的 👍👎)是独立 endpoint,照常工作
  • 信箱新增「📷 快照日历」入口SnapCalendarScreen.tsx(pageSheet modal,模仿 InboxScreen/StickyWall 同款架构)—— 月份切换器 + 7 列日历网格 + Polaroid 缩略图 cell + 全屏预览 overlay + 收起 pill;today cell 粉边框;网格 cell 单 polaroid / 双 polaroid 错位叠放(两人都拍时我的在上 +2° / ta 的在下 -2°);MailboxScreen 在「📝 小贴吧」下方加 entry card,ref + onRefresh 联动 reload;复用现有 GET /api/snaps?month=YYYY-MM(反窥探:caller 没拍那天 partner_photo 不下发)
  • 死代码清理:客户端 api.openCapsule(v1.2.17 后改 auto-open sweep 替代)+ api.logout(被 revokeSessionGroup 替代);服务端 dbOps.pairUsers / unpairUsers(被 pairCouple / unpairCouple 取代)+ dbOps.getSession / getRituals / getAllPairedUserTokens / saveSnap + stmtGetRituals / stmtGetAllPairedTokens SQL 全清
  • regression_v1_2_21.test.ts +24 / 273 tests passed

v1.2.20(2026-05-22)

表情连发去重(×NN bounce)+ 长按标题自定义跟随账号

  • 同表情连发去重(屏幕内):HistoryScreen 新增 collapseBursts() —— 同 user_id + 同 action_type + 距上一条 ≤ 5 min 折叠成一个 burst,leader 的 id 作 React key 保持稳定,displayCreatedAt / latestId 滚到最新成员;在 groupByDate 内部 per-day 调用防止跨日误并;injectUnreadDivider 改用 (latestId ?? id) 比 boundaryId
  • ActionRecord ×NN 徽章count > 1 时 emoji 后渲染 ×NN,prevCountRef + Animated.sequence,只有 count 严格变大才弹跳,历史回放 "kiss ×3" 加载时不会无端跳
  • 长按标题自定义users.history_title 新列(TEXT NOT NULL DEFAULT ''),空串=用默认;PUT /api/profile 加 optional history_title 字段:trim / 30 字上限 / non-string 400 / whitespace-only 视为清空 / undefined 不动现有值(老客户端兼容);GET /api/status 带字段;客户端 storage HISTORY_TITLE_KEY + App.tsx 三处 caching 全部同步;HistoryScreen 标题 onLongPress → Alert.prompt → updateProfile,DEFAULT_HISTORY_TITLE = '香宝聚集地 💕'
  • 27 个新回归测试(228 → 255)

v1.2.19(2026-05-21)

弱网误踢 + 登录 ID autofill

  • 弱网误踢回登录页:服务端 /api/auth/refresh rotation grace window 10 s → 300 s(地铁 / 电梯 / 弱 Wi-Fi 一个 round-trip 常超 10 s);revoked session 仍在 grace 检查之后立即返回 code:session_revoked,强制下线保证不变。客户端 App.tsx bootstrap 在 generic AuthError 上等 2.5s 再 retry 一次 getStatus 才决定是否清账号;抽 applyStatus / fallbackToCachedOrWaiting 让首试和 retry 路径复用
  • 重新登录时 ID 不能 autofill:SetupScreen 登录表单 loginIdtextContentType="username" + autoComplete="username"loginPasswordtextContentType="password" + autoComplete="password",iOS QuickType 栏现在能在 ID 字段建议保存的凭证
  • 11 个新回归测试(217 → 228)

v1.2.18(2026-05-21)

4 项打磨:每推送 +1 角标 / 登录后时区生效 / tab 红点 race / 收件箱排序

  • APNs badge 每次 +1users.unack_push_count 新列,pushToUser 每次 INCREMENT,最终 badge = max(1, 跨功能未读数, 该计数)POST /api/badge-ack reset 计数;mark-read 也 reset
  • 登录后时区生效:App.tsx bootstrap / waiting-state poll / handleRegistered 都拉 status.timezone + partner_timezone + partner_remark 存进 AsyncStorage,确保首屏 WriteLetterScreen 预览 / InboxScreen 邮戳 / MailboxScreen 下次送达提示用真实 tz 不再 fallback Asia/Shanghai;SettingsScreen.loadStatus 也同步进 storage 让跨设备改完 tz 另一台 focus 即生效
  • 信箱 tab 红点二次加固GET /api/capsules auto-open sweep 改用 autoOpenCapsuleopened_at 写成 unlock_at 而非 CURRENT_TIMESTAMP,避免 race 把刚自动 open 的 capsule 再次标成新到达;语义也更对(「收于」邮戳 = 信件实际送达时刻)
  • 收件箱排序明确化:3 个 e2e sort 测试 —— 单纯按到达 / 同 session 多封信按写信时间 tiebreak / 混合 mailbox + 新解锁 capsule 也能让 capsule 落顶层
  • 14 个新回归测试(203 → 217)

v1.2.17(2026-05-21)

3 bug + 2 优化:择日达对方收不到 / tab 红点污染 / 重登多设备 / 分钟级解锁 / 收件箱排序

  • BUG1 择日达对方收不到GET /api/capsules 加 auto-open sweep —— 到点的 capsule 在 recipient 拉列表时直接 open,对齐半日达「session reveal 自动揭晓」语义;修了「openCapsule endpoint 客户端从未调用 + InboxScreen 直接过滤掉 opened_at=null 的 capsule」造成的死锁
  • BUG2 写信后信箱 tab 红点:App.tsx 去掉 hasFreshOutboxItems + subscribeOutboxChanged 对 tab dot 的驱动,tab 红点只由 partner→me 信号(sticky + inbox 未读)驱动;发件箱 🚩 入口卡红旗由 MailboxScreen 内部状态维持不受影响
  • BUG3 重登后「新的本机」:用 APNs device_token 作「同物理设备」唯一可信信号,/loginPUT /device-token 检测到 token 已绑定到本用户旧 session 时自动 revoke 那个旧 session 并把 device_name + is_primary 继承过来;两台同名 iPhone(APNs token 不同)依旧并存;客户端 storage getApnsTokenCache / setApnsTokenCache(device-scoped,不参与 clearAll)
  • REQ1 择日达分钟精度:scheduler 去掉 utcMin % 5 === 0 节流,capsule unlock 每分钟扫一次;notified_at + fireOnce 分钟桶仍保证不会重复推送
  • REQ2 收件箱排序:LetterCard 加 writtenAt 字段;排序改 (arrivedAt ASC, writtenAt ASC) —— 同 session 揭晓的多封信中,后写的落屏幕底部 = 开信箱第一眼看到
  • 22 个新回归测试 + 全部 181 个老测试通过(203/203)

v1.2.16(2026-05-10)

README docs 同步

  • 测试数从 stale 的 130 同步成实际的 181(技术栈描述 + 跑测试说明 两处)
  • 项目结构 components/ 列表移除已删的 MailboxCard / TimeCapsuleCard,补上 DailyQuestionCard / DailySnapCard / RitualButton / TouchArea / ActionRecord / ReactionPicker
  • 项目结构 utils/ 列表把不存在的 outboxFresh 改成实际的 outboxEvents,补上 device / timezone
  • v1.2.14 changelog 把 "L8 删除 350 行" 修正为 "~950 行"(实际 433+516=949)
  • Roadmap backfill v1.2.7—v1.2.11 五个版本(之前从 v1.2.6 直接跳到 v1.2.12,缺了 sessions 多设备打磨那批)

v1.2.15(2026-05-10)

4 处验收清单测试覆盖补齐

  • GET /health 加 supertest + 静态检查(生产 index.ts 注册的契约 {status:'ok',timestamp} 必须对得上)
  • L5 客户端 SetupScreen.tsx 静态检查 password.length < 6 + Alert 文案「密码至少6位」+ placeholder「至少6位」
  • H3 客户端 DailyQuestionCard / DailySnapCard 静态检查 subscribe('daily_update', ...) + 各自的 kind/target 过滤分支 + load() 真的调到(防 no-op stub regression)
  • POST /api/snaps 显式 happy-path 测试(前一版只在 H3 emit 测试里隐式覆盖,新加一条独立断言"L1 multer wrapper 没把正常上传也变 400")
  • 181 / 181 测试全过(171 → 181,+10)

v1.2.14(2026-05-10)

16 项 M / L 级审计修复

  • M1 /api/dates 未配对早返回字段名 nearestpinned,与 paired 路径一致
  • M2 /api/profile partner_remark 提交前 trim:whitespace-only 清空备注、超长按 trim 后判
  • M3 HistoryScreen 30s 兜底轮询加 per-focus stale flag — 切 tab 时 in-flight fetch 不再 setState 出 RN warning
  • M4 time_capsules / bucket_items UPDATE 加 AND pair_id = ? 防御层(既有路由 pre-check 之外的二道防线)
  • M5 WriteLetterScreen pendingSaveRef — 关→快速重开模态时先 await 上一次 in-flight 草稿 PUT 再 GET,避免读到 stale draft
  • M6 AsyncStorage getter 全部走 safeGet() wrapper,存储底层抛错时返回 null 而不是 unhandled rejection 卡死 loading
  • M7 push.ts INVALID_TOKEN 清理移除 break — 单 token 时行为不变,未来改 batch 时每个失效 token 都被清
  • M8 socket client subscribe unsubscribe 后 if (set.size === 0) delete listeners[event] — listeners 对象不再线性 grow
  • L1 /api/snaps multer error 用 wrapper 转 400 JSON,避免客户端把 multer 500 误判为网络问题死循环重传
  • L2 App.tsx bootstrap getHistory(1) 加 cancelled flag,rapid 退出登录时已发出 fetch 不再写 ref
  • L3 /api/ws-ticketif (!user.partner_id) return 400,未配对时不再发空 ticket
  • L4 startScheduler 检查 SCHEDULER_DISABLED=1 环境变量,pm2 cluster 模式时可指定单进程跑调度,防多进程重复推送
  • L5 /register password 最小 4 位 → 6 位,客户端 SetupScreen 同步 placeholder + alert
  • L6 push.ts_resetAPNsForTesting() export — 测试隔离更干净
  • L7 notification.ts 新增 getNotificationPermissionStatus() 暴露 granted/denied/undetermined,Settings UI 后续可加"通知权限被禁用"提示
  • L8 删除 v1.1 重构后未再被 import 的 MailboxCard.tsx / TimeCapsuleCard.tsx 死代码(共 ~950 行)
  • 24 个新回归测试(171 / 171 全过;上一版 147)

v1.2.13(2026-05-10)

4 项 HIGH 级审计修复

  • H1 HistoryScreen 用 normalizeIso 替代 + 'Z' 拼 SQLite 时间戳:Hermes 解析 '2026-05-10 12:34:56Z' 不稳,跨时区情侣可能看到错位时间或所有消息挤进同一日期分组
  • H2 App.tsx coldStartConsumedRef 在 appState 离开 'ready' 时复位:解决"强制下线 → 重新登录" 同进程内点通知不再跳转到对应 tab 的问题
  • H3 服务端 3 个 endpoint 新增 emit('daily_update', ...) (/daily-question/answer / /snaps / /daily-reaction),DailyQuestionCard / DailySnapCard 精确订阅:双方同在「每日」tab 时一方答完题对方屏幕立刻刷新
  • H4 socket 客户端 connect_error handler 入口捕获 myInstance,await 后比对 socket === myInstance 守卫:AppState 后台→前台触发 disconnect+connect 时不再把旧 handler 的新 ticket 串到新 socket
  • 10 个新回归测试(137 → 147)

v1.2.12(2026-05-10)

「半日达」更名 + 信箱底部下次送达预告

  • 全仓库 次日达半日达:4 个 APNs 推送模板(mailbox_open / mailbox_written / mailbox_countdown_15min / mailbox_reveal)+ App 全部 UI 文案(写信选项卡 / 收件箱空态 / 废件箱 / 发件箱 / Mailbox 入口卡两条副标题 / 字数超限 Alert / 老 MailboxCard 标题与列表 chip)+ README 描述文 + 内部注释,无遗漏
  • 信箱页 写信 ✉️ pill 下方新增小字「下个半日达将于 北京时间 05-10 20:00 寄达」,与每日页「下次更新于 ...」同款 typography(fontSize 12 / COLORS.textLight / 居中),按 Settings 里设置的 tz 渲染
  • 新增 useNextHalfDayRevealAt() countdown hook:对齐到下一个 0:00 / 12:00 UTC 边界(半日达 reveal 节奏),minute-tick 重渲染;和 useNextDailyRefreshAt 共用同一思路
  • 137 个接口测试全过(之前 README 标 130,已同步徽章)

v1.2.6(2026-05-07)

APNs badge 全推送覆盖

  • pushToUser 在 caller 没传 badge 时兜底取真实跨功能未读数(History + 信箱 + 已解锁 capsule + 小贴吧 partner block 累加,floor=1,确保至少亮红圈)
  • scheduler.broadcastPush 改为按用户 fan-out 走 pushToUser(之前直接调 sendPush 绕过 badge 兜底,定时广播全部漏掉 badge)
  • mailbox.created_at(SQLite 默认 'YYYY-MM-DD HH:MM:SS')vs inbox_last_seen(ISO 带 T Z)lex 比较坑用 datetime() 归一化( < T 否则 marker 永远显得更晚,未读永远算 0)
  • 测试 6 处 mockPush 断言补 badge arg;总数 130 个接口测试 pass

v1.2.5(2026-05-06)

多设备 + 跨端同步

  • 多设备会话管理refresh_tokens 拓展 per-session 元数据(device_name / device_model / device_os / app_version / last_active / is_primary / revoked);新增 /api/sessions GET / POST /sessions/:sid/primary / DELETE /sessions/:sid;auth 中间件每请求过 isSessionActive 让强制下线即时生效;DeviceListCard 在 Settings tab 列出所有设备 + 主设备徽章 + 本机徽章 + 一键切主设备 / 强制下线
  • 多设备 APNs 推送device_tokens 表(apns_token PK + user_id + session_id + updated_at)替代 users.device_token 单值;pushToUser fan-out 到一个用户的全部 token;session 注销时同步删 token;APNs 410 失效自动 evict
  • 跨端状态全部上服务端daily_seen_date / pa / psinbox_last_seenoutbox_last_seenwrite_letter_draft 改成 users 表字段,多设备状态实时一致;only-advance UPDATE 守卫旧客户端不会 roll back 已读游标
  • 上传可靠性:snap 上传 401 主动续期;超大 multer payload catch 兜底;429 不再误判登出(区分限流 vs 真鉴权失败)

v1.2.4(2026-05-04)

8 项稳定性修复

  • 历史按用户 tz 分组(之前混用 UTC,跨时区双方按日聚合错位)
  • streak 跨 tz 计算(连续天数在用户夜里跨日时不再丢一天)
  • snap 上传 race(同一秒两次上传 atomic rename 抢占 → 改为 DB pre-check + tmp 文件唯一名)
  • scheduler 并发推送(Promise.allSettled 替代串行 await,单个慢 token 不再卡掉整波广播)
  • 删冗余轮询(HistoryScreen + App.tsx 两份 10s poll,合并为单 socket 驱动)
  • API 超时(默认 30s 超时 + AbortController,挂机进程不会无限 pending)
  • 401 主动续期(access token 过期请求触发一次 refresh 再重发,避免对用户表现为"一直转圈")
  • 上传 catch 兜底(multer 抛异常时返回 400 而不是 unhandled crash)

v1.2.3(2026-05-04)

  • 快照反窥探:对方拍了今天的快照、自己还没拍时,服务端不返回对方的照片 URL(避免"先看对方再决定自己拍什么")
  • 新表情praise_you(蒸蚌)+ 天气分类 5 个(晴 / 多云 / 雨 / 雷 / 雪)+ haha(嘻嘻)+ sad(难过)

v1.2.2(2026-05-03)

7 项屏幕响应式适配:爱心心跳尺寸 / 触摸圆环半径 / 写信 pill 宽度 / 卡片高度 / 信纸字号上限 / iPad 信件最大宽度 / 顶部状态条间距,从 iPhone SE 到 iPad mini 全覆盖。

v1.2.1(2026-05-03)

  • 时区一致性(每日刷新点 / 收件箱「写于」「收于」/ 早晚安同日合算 / 跨 tz 校验)
  • 邮戳精度(双时区按各自 tz 分别格式化)
  • 标题渐变贴边(每日 / 信箱 / 约定 / 数据顶部渐隐贴住屏幕边)
  • 早晚安同日规则测试

v1.2.0(2026-05-03)

pair_id 关系实体 + 90 天数据 TTL

  • 新增 couples 表,每段「关系」抽象为稳定 10 字符 pair_iduser_a_id < user_b_id lex 排序保证唯一)
  • 所有 couple-scoped 数据(actions / mailbox / time_capsules / sticky_notes / important_dates / daily_answers / rituals / inbox_actions)打 pair_id 标签
  • 解绑写 ended_at 进入 90 天 grace 期,期间重绑同一对人 → ended_at 清空,全套数据自动复活;中间换过别的对象再换回来不会读到错误关系的旧数据
  • TTL 调度器 03:00 UTC 每日扫一次过期 couple,按 pair_id 级联硬删
  • pair / unpair 整体事务化 + backfill 脚本一次性 migrate 历史数据 + 6 个新测试

v1.1.8(2026-05-03)

信箱大改造

  • 半日达单场可多发(不再「一场一封」)
  • 写信成功 toast + 信箱 tab 红点直接点亮 + 发件箱排序倒置
  • 新增 OutboxScreen 上下滑动浏览未送达;废掉曾试过的右划撤回(手势冲突 + 用户预期不一致)
  • 时区一致性 + 每日刷新加日期去秒 + 快照评价加标签 + 快照反应内联

v1.1.6(2026-05-01)

  • 跟帖后不再自动续开编辑器(写完一条对方再开会被强制再写一条的反人性)
  • 空草稿不持久化到服务端 temp(避免脏数据残留)
  • 单条跟帖可独立撕(之前必须撕整张 sticky)

v1.1.5(2026-04-30)

安全 / 竞态打磨

  1. Socket 触摸 DoS — 客户端可以无限循环 emit touch_start,服务端原地放大成 APNs 推送 + 心跳广播。新增 5/1s 滚动窗口限速 + silent drop。
  2. 客户端 socket 死循环 — auto-reconnect 在网络抖动时进入「连上即断」自激振荡,每秒新建几十条连接。改为指数退避 + 短窗口熔断。
  3. Capsule 推送重复 — 调度器进程重启或被 cron 多次拉起时,unlock_at <= now AND opened_at IS NULL 会再次命中已通知的胶囊。新增 notified_at 列 + 内存 minute-bucket dedup key。
  4. Refresh token 轮换非事务 — 旧 hash 删除与新 hash 写入分别为两条 SQL,崩在中间会让用户彻底登不回来。合并到单事务。
  5. Presence 闪烁 — 网络抖动时短间隔 disconnect/reconnect 触发 presence_singlepresence_both 来回闪。checkBothOnline() 加 3s debounce。
  6. 跟帖 vs 撕贴竞态 — 一方正在写跟帖、另一方撕走整张 sticky,commit 时若 sticky 已不存在会留下孤儿 block。在 commit 前校验 sticky 存在 + 撕贴 cascade 删除。
  7. AppState 切换定时器泄漏 — DailyQuestionCard / DailySnapCard / 触感震动定时器在切换 tab / 切到后台时未清理;统一加 cleanup。
  8. Presence stale-closure — 旧 disconnect 闭包在 presence 被新 session 覆盖后还会广播,把刚回来的人误标成离线。closure 内通过身份 token 比对识别 presence 是否仍属于自己。
  9. 写信草稿覆写 sealing 内容 — 用户高速点「寄出」时,草稿 setter 还会再触发一次,把 sealing 阶段的 UI 草稿(已封存内容)回写到状态。setter 在 sealing 之后失活。

v1.1.0 — v1.1.4(2026-04-30)

信箱大重构

  • 平铺卡片 → 三入口(收件箱 / 废件箱 / 小贴吧)+ 写信 pill;写信下沉成统一流程
  • 「垃圾篓」改名「废件箱」
  • 收件箱重构:点击空白收起 + 底部「收起」pill + 标题贴顶 + 卡片倒序按时间 + 旧贴 relayout / 旋转 ±1°-±5° / 时间戳浮在最上
  • 写信流程 5 阶段:write → sealing → kind → capsuleDetails → sending;封信 + 寄出过场动画
  • 择日达分钟级日期时间选择(六年窗口 + 月日联动 clamp + 双时区即时预览)
  • 写信键盘可收回 + KeyboardAvoidingView 防遮挡 + 草稿持久化 + 正式信件版式(致 / 落款 / 双时区邮戳)

小贴吧(信箱 tab 共享便利贴墙)

  • 木色背景 + 奶油便利贴 + 自己粉 / 对方蓝
  • 掉落入场动画 / 撕下来过场动画 / 永久删除
  • 跟帖独立成订书钉串联的便利贴堆 + 老贴一并 relayout
  • 跟帖纸 z-order 翻转 + 圆钉替代订书钉
  • 双击便利贴跟个帖 + 编辑器 tap-外保留草稿
  • 标题边缘真正贴住的柔化渐隐 + 选中态加辨识 + 「不看了」pill
  • 冷启动误重登修复 + 收件箱未读小旗子 + 重连给孤身 socket 同步 presence_single

v1.0.2 修复清单(Release notes

经仓库全量审计验证的 7 个真实漏洞:

  1. 徽章数被反应(reaction)污染stmtCountUnreadActions / stmtLatestPartnerActionId 没过滤 reply_to,反应行 id > 顶层 id 时 markRead clamp 不到,徽章可能卡在 ≥1 永远清不掉。两条 SQL 加 reply_to IS NULL
  2. 登录后用户名丢失/api/login 不返回 name,重装登录的用户在 MailboxCard / InboxScreen / TimeCapsuleCard / HistoryScreen 全部显示「我」。服务端响应增加 name,客户端登录写入。
  3. 收件箱「未读」红标错闪seenBeforeOpenRef 异步加载与 load() 拉信件并行,AsyncStorage 解析慢时所有卡片瞬间打红标。改为先 await getInboxLastSeen() 再触发 load()
  4. 多设备 socket presence 误报下线presence.socketsMap<userId, socketId>,结构上无法表达多设备。重构为 Map<userId, Set<socketId>>
  5. 未开启的择日达扔进垃圾篓后永远消失/api/inbox/trash 加守卫:未开启的胶囊不能被扔。
  6. 接收方触感震动可能停不下来 — 新增 AppState 监听:切到非 active 时主动清空所有定时器和动画。
  7. 倒计时 1Hz 无条件重渲染 — DailyQuestionCard / DailySnapCard 改为 cooldown 内才 tick,过期自停。

v1.0.1 修复清单

  • HIGHPOST /capsules/:id/open 不检查 inbox_actions 状态:彻底删除一封 capsule 后仍可通过此 endpoint 直接拿回内容。已加 status 检查(outgoing partner-vis 豁免)。
  • MEDIUMEnvelopeOpenAnimation 缺动画取消机制:快速 open/close/open 切换时旧动画 callback 仍触发 setState,造成闪烁。已加 cancelled flag。
  • LOW-MEDIUMIslandToast unmount 时 hideTimer 未清理:可能 setState on unmounted component。已加 useEffect cleanup。
  • LOW — scheduler 给 self-vis 胶囊推送 partner:partner 收到 fake 推送但 app 内看不到对应胶囊。已在循环里跳过。

项目结构

PoopHub/
├── couple-buzz-app/                   # Expo / RN 移动端
│   ├── App.tsx                        # 入口 + 6 tab 路由 + PillTabBar + linking + push/socket 生命周期
│   ├── app.config.ts                  # Expo + EAS 配置
│   ├── eas.json                       # EAS Build profile
│   ├── src/
│   │   ├── screens/                   # Home / History / Us / Mailbox / WriteLetter / Inbox / Outbox / Trash / StickyWall / SnapCalendar / AnniversaryWish / Settings / Setup
│   │   ├── components/                # DailyQuestionCard / DailySnapCard / RitualButton / TouchArea / BucketListCard / SealAnimation / EnvelopeOpenAnimation / IslandToast / SpringPressable / StickyNote / FireworksOverlay / DeviceListCard / WeeklyReportCard / StatsCard / ActionRecord(ReactionPicker 在 v1.2.21 整文件移除)
│   │   ├── services/                  # api(含 sessions / sync / outbox / letter-draft / dates PUT / history before_id 翻页)/ socket(含 daily_update 转发)/ notification
│   │   ├── utils/                     # storage(含 HISTORY_TITLE / APNs_TOKEN_CACHE keys)/ countdown / postmark / inboxUnread / outboxEvents / toolbarSlot / device / timezone
│   │   └── constants.ts               # 颜色 / 表情配置
│   └── targets/widget/                # iOS WidgetKit Swift 框架(expo-target.config.js + index.swift,数据桥未接通)
│
└── couple-buzz-server/
    ├── src/
    │   ├── index.ts                   # Express 入口 + 中间件 + 限流
    │   ├── routes.ts                  # REST API(~70 路由:含 sticky-wall 11 个接口 + inbox trash/restore/purge + sessions 五件套(list/primary/name/group/single)+ sync 三组游标 + outbox + letter-draft + dates PUT/pin/delete + history before_id 翻页 + badge-ack + logout-all + capsules auto-open sweep)
    │   ├── socket.ts                  # WebSocket touch / presence / action / sticky / daily_update(多设备 Set + 重连快照 + 5/1s 限速 + stale-closure guard)
    │   ├── auth.ts                    # JWT / scrypt / HMAC 图片签名 / refresh 并发锁 + 事务轮换 + per-session 元数据 + isSessionActive + 5min refresh grace
    │   ├── bursts.ts                  # 锁屏推送合并 ×N collapse-id(5min 滚动窗口 per (recipient, sender, action_type))— v1.3.1 新增
    │   ├── db.ts                      # SQLite schema + 全部 SQL 操作(couples / device_tokens / sticky_* / inbox_actions / time_capsules.notified_at / users.{daily,inbox,outbox,letter}_seen + users.history_title + users.unack_push_count / refresh_tokens.session_id+device_*+is_primary+revoked / actions.reply_to 列保留但 reactions 功能已删)
    │   ├── push.ts                    # APNs + 推送模板(payload 包 body + badge 全覆盖兜底 + 多设备 fan-out + 410 自动 evict + PUSH_MESSAGES export 让 routes 复用模板组合 body)
    │   ├── scheduler.ts               # 信箱 reveal / 胶囊解锁(分钟级精度,去掉 5min 节流)/ 周报 / couple TTL 03:00 硬删(capsule 解锁 dedup key)
    │   └── questions.ts               # 1000+ 每日问答题库
    ├── docs/BACKUP.md                 # GPG 加密备份完整运维指南
    ├── scripts/backup.sh              # GPG 加密备份脚本
    ├── scripts/secure-existing-backups.sh  # 一次性处理历史明文备份
    └── data/                          # 运行时 DB 与 snap 上传目录(gitignore)

本地开发

前置

  • Node.js 20.x
  • Xcode + iOS 模拟器(或 Expo Go / EAS Development Build)
  • Apple Developer 账号(启用 APNs 推送时需要)

启动后端

cd couple-buzz-server
npm install
cp .env.example .env        # 填 JWT_SECRET、APN_* 等
npm run dev                 # ts-node + nodemon

服务默认监听 127.0.0.1:3000。首次启动自动建表 + 跑 schema migration,上传图片放 data/snaps/

启动 App

cd couple-buzz-app
npm install
npm run ios                 # 或 npm start 扫码

首次登录两端配对:A 注册后获得 6 位 ID(去掉容易混淆的字符),B 注册后在配对页输入 A 的 ID 即可绑定。

跑测试

cd couple-buzz-server
JWT_SECRET=test-secret npm test
# 15 suites / 366 passed

环境变量

couple-buzz-server/.env

Key 说明
PORT HTTP 端口,默认 3000
HOST 绑定地址,默认 127.0.0.1
JWT_SECRET JWT 签名密钥(必填,未设置启动会主动报错)
APN_KEY_ID Apple APNs Key ID
APN_TEAM_ID Apple Team ID
APN_KEY_PATH .p8 私钥路径,默认 ./certs/AuthKey.p8
APN_BUNDLE_ID iOS Bundle ID
APN_PRODUCTION 生产 APNs 网关开关;Ad Hoc 安装的 IPA 需要 true

couple-buzz-app/.env

Key 说明
API_URL 后端地址。留空 / 删除 会兜底到 app.config.ts 默认值(生产 URL)

本地调试连本地服务时,建议用 API_URL=http://192.168.x.x:3000 npm start inline 注入,不要修改 .env 文件 —— 避免脏数据被 EAS Update 烤进生产 bundle。


部署

服务端

  • 自托管 Node.js 进程(pm2 守护),SQLite 单机数据库
  • 反向代理(nginx / Caddy)终止 HTTPS
  • scripts/backup.sh 配合 cron 每天 03:00 跑 GPG 加密备份(保留最近 30 份),私钥离线保管
  • 异地备份:Mac 端 launchd 23:00 用 rsync 拉到 iCloud Drive(私钥可解密)

详细备份运维流程见 couple-buzz-server/docs/BACKUP.md

移动端

  • 首次 / native 改动eas build --profile preview --platform ios,出 internal distribution IPA,Ad Hoc provisioning profile 直接安装
  • 后续纯 JS 改动npm run ota:preview(仓库脚本 inline 注入生产 API_URL

OTA 推送后手机端冷启动两次生效。


Roadmap

v1.3.8(2026-05-27,Latest

  • 长信滑到边界牵动收件箱整体 — letter overlay 改 transparent Modal(=overFullScreen)盖住 pageSheet InboxScreen,吞掉 swipe-to-dismiss 手势;regression_v1_3_8 +6 / 366 全过

v1.3.7(2026-05-27)

  • 4 项审计修复 — 半日达字数 500→1000 / daily_update socket 转发 / 纪念日编辑 API + UI / cross-pair lookup pairId 防御层;+13 测试 / 360 全过

v1.3.3 → v1.3.6(2026-05-27)

  • 长信完整可读 — flex 链替代硬编码 200pt reserve(iPhone SE / Dynamic Type / 长备注全部覆盖)+ 滚动 affordance 3 件套(indicator / 底部渐隐 / 「拖动阅读」copy)+ fadeOpacity 双分支避免 inputRange 重复边界 + envelopeWrap pointerEvents 改 box-none + deleteSticky 5-arg → 2-arg pair_id 适配;+30 测试

v1.3.0 → v1.3.2(2026-05-22)

  • 表情连发推送合并bursts.ts 5min 滚动窗口 + APNs collapse-id,锁屏 5 个 💋 spam 只显示 1 条 ×5 banner
  • 废话区上拉翻页before_id cursor + has_more + maintainVisibleContentPosition + 单次拖动至多触发一次 loadOlder
  • 快照日历 ta|我 toggle + 点空白收起 + 反窥探 4 场景测试
  • +63 测试

v1.2.21(2026-05-22)

  • 信箱新增「📷 快照日历」入口 — pageSheet modal + 7 列网格 + Polaroid 缩略图 + ta/我 单视角
  • 「废话区反应」功能完全移除 —— server route + dbOps + push 模板 + ReactionPicker.tsx + ActionRecord onLongPress 全删;daily-reaction 独立保留
  • 8 处死代码清理 —— openCapsule / api.logout / pairUsers / unpairUsers / getSession / getRituals / getAllPairedUserTokens / saveSnap

v1.2.20(2026-05-22)

  • 表情连发屏幕内 ×NN 去重 + bounce — 5min 滚动窗口 collapseBursts
  • 长按 HistoryScreen 标题自定义users.history_title 列 + PUT /profile 字段 + 跨设备同步

v1.2.19(2026-05-21)

  • 弱网误踢修复 — refresh grace 10s→300s + bootstrap 2.5s retry
  • 登录 ID autofill — TextInput textContentType + autoComplete

v1.2.18(2026-05-21)

  • 每推送 +1 角标users.unack_push_count 列 + pushToUser INCREMENT + POST /api/badge-ack
  • 登录后时区生效 — bootstrap / waiting poll / handleRegistered 全部 cache tz 到 storage
  • 信箱 tab 红点二次加固 — auto-open sweep opened_at = unlock_at 避免 race
  • 收件箱排序(arrivedAt ASC, writtenAt ASC) 复合 + scroll-to-bottom + 3 个 e2e 测试

v1.2.17(2026-05-21)

  • 择日达对方收不到修复GET /api/capsules auto-open sweep + 客户端 InboxScreen 过滤掉 opened_at=null capsule 死锁修复
  • 写信后信箱 tab 红点污染修复 — 去掉 outbox 触发 tab dot
  • 重登多设备「新的本机」修复 — APNs device_token 作物理设备身份;自动 revoke + 继承 device_name + is_primary
  • 择日达分钟级解锁 — scheduler 去掉 5min 节流
  • 收件箱排序(arrivedAt, writtenAt) 同 session 后写的落底部 = 开信箱第一眼

v1.2.16(2026-05-10)

  • README docs 同步 — 测试数 stale 130→181 / 项目结构 components+utils 实际化 / v1.2.14 行数 350→950 修正 / backfill v1.2.7—v1.2.11

v1.2.15(2026-05-10)

  • 验收清单测试补齐/health / SetupScreen 客户端密码校验 / DailyCard 客户端 subscribe / 快照上传 happy path 显式断言 + 10 个新测试

v1.2.14(2026-05-10)

  • 16 项 M / L 级审计修复/api/dates 字段一致性 / partner_remark trim / HistoryScreen 轮询 stale flag / pair_id 防御层 / 草稿 PUT 串行化 / AsyncStorage 容错 / socket listeners 自动收尾 / multer 错误 400 化 / scheduler env gate / 密码 ≥ 6 位 / 删死代码 350 行 / 24 个新回归测试

v1.2.13(2026-05-10)

  • 4 项 HIGH 级审计修复 — HistoryScreen normalizeIso / coldStartRef 复位 / 每日 socket 实时同步 / socket connect_error 实例守卫 + 10 个新回归测试

v1.2.12(2026-05-10)

  • 「半日达」更名 — 全仓库 26 处 次日达半日达(4 个推送模板 + App 全部 UI 文案 + README + 注释)
  • 信箱底部下次送达预告 — 写信 pill 下方"下个半日达将于 ... 寄达",按用户 tz 渲染,对齐 0:00 / 12:00 UTC 边界

v1.2.7 — v1.2.11(2026-05-07 — 05-08)

  • v1.2.11 fix(perf+ux) — 8 处审计发现的边缘问题
  • v1.2.10 fix(sessions) — 登录继承设备名 + 暴露本机自我提升主设备
  • v1.2.9 fix(sessions) — 整组原子下线,修退出后残留 session 重现为「另一台设备」
  • v1.2.8 feat(sessions) — 已登录设备按设备去重 + 自定义设备名
  • v1.2.7 docs — 全面更新 README 反映 v1.1.6 → v1.2.6 全部变更

v1.2.6(2026-05-07)

  • APNs badge 全推送覆盖pushToUser 兜底真实未读数(floor=1),scheduler 广播也走同一管道,~25 种推送类型桌面图标都会变红圈

v1.2.5(2026-05-06)

  • 多设备会话管理 — DeviceListCard / 主设备制 / 强制下线 / isSessionActive 即时生效
  • 多设备 APNs 推送device_tokens 表替代单值,pushToUser fan-out 全设备
  • 跨端状态全部上服务端 — daily_seen / inbox_seen / outbox_seen / letter_draft 全部服务端字段,only-advance SQL 守卫
  • 上传可靠性 — 401 主动续期 + 超时 catch 兜底 + 429 不再误判登出

v1.2.0 — v1.2.4(2026-05-03 — 05-04)

  • pair_id 关系实体 + 90 天数据 TTL + 重绑数据复活(v1.2.0)
  • 8 项稳定性修复 — 历史按用户 tz 分组 / streak 跨 tz / snap 上传 race / scheduler 并发推送 / 删冗余轮询 / API 超时(v1.2.4)
  • 快照反窥探 + 蒸蚌/天气分类/嘻嘻/难过 表情(v1.2.3)
  • 7 项屏幕响应式适配(v1.2.2)
  • 时区一致性 / 邮戳精度 / 标题渐变贴边 / 早晚安同日规则(v1.2.1)

v1.1.6 — v1.1.8(2026-05-01 — 05-03)

  • 跟帖 3 处修复 — 不再自动续开 / 空草稿不持久化 / 单条可撕(v1.1.6)
  • 信箱大改造 — 半日达多发 / OutboxScreen 上下滑 / 写信成功 toast / tab 红点直点 / 时区 / 快照评价加标签(v1.1.8)

v1.1.5(2026-04-30)

  • 9 项安全 / 竞态打磨:socket touch 限速 / 客户端死循环熔断 / capsule 索引 + notified_at 持久 dedup / refresh token 事务化轮换 / presence 防闪 / 跟帖 vs 撕贴竞态 / AppState 定时器 cleanup / presence stale-closure guard / 写信草稿覆盖 sealing 阶段

v1.1.0 — v1.1.4(2026-04-30)

  • 信箱大重构:三入口(收件箱 / 废件箱 / 小贴吧)+ 底部固定写信 pill
  • 统一写信流程:write → sealing → kind → capsuleDetails → sending 五阶段,封信 + 寄出过场动画
  • 正式信件版式:致 / 落款 / 双时区邮戳,奶油色信纸 + 棕墨字
  • 择日达分钟级日期时间选择:年/月/日/时/分五段下拉 + 月日联动 clamp + 双时区即时预览
  • 写信键盘可收回 + 草稿持久化 + KeyboardAvoidingView 防遮挡
  • 小贴吧:双方共享便利贴墙;木色背景 + 奶油纸 + 双色字
  • 跟帖 = 订书钉串联的便利贴堆 + 圆钉装饰 + 老贴 relayout
  • 掉落入场动画 + 撕下来过场动画 + 永久删除
  • 双击便利贴跟个帖 + 编辑器 tap-外保留草稿

v1.0.2(2026-04-28)

  • 自定义灵动岛 PillTabBar + spring + onPressIn 瞬切 + 4 tab 红点
  • App 级 Toolbar Slot + 顶部 scroll-bound fade
  • 通知 deep-link 路由(cold-launch + warm-tap 双路径)
  • APNs payload body wrap + iOS 锁屏通知合并
  • 7 个真实漏洞修复 + 95 接口测试

v1.0.1(2026-04-28)

  • 信箱模块重命名:树洞信箱 → 半日达、时间胶囊 → 择日达
  • 写完后双方都看不到信件内容直到送达 + 封信 / 开信过场动画
  • 第 6 个 tab「🎀 约定」:纪念日 + 心愿清单合并管理
  • 收件箱(Apple Wallet 风格)+ 垃圾篓
  • 4 个安全/逻辑漏洞修复

v1.0.0(2026-04-27)

  • MVP:5 tab 结构、双场信箱、couple ID、APNs 推送、JWT 双 token、95 接口测试

致谢

为我和我的另一半而做。如果你也想给伴侣做一个,欢迎 fork。

许可

MIT License — 自由使用 / 修改 / 商用,唯一要求是保留版权声明。

About

情侣专属 iOS App · 50+ 表情推送 · 每日仪式 · 时间胶囊 · 加密备份 · React Native + Expo + Node.js

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages