[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-im":16,"notes-page-im-01-message-state\u002F03-pagination-search":66,"i-ri:arrow-left-s-line":210,"i-ri:menu-2-line":212,"i-ri:arrow-right-s-line":214},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38,42,46,50,54,58,62],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Fim\u002F01-message-state\u002F01-offline-sync","连接恢复了，离线消息为什么还没补齐？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Fim\u002F01-message-state\u002F02-read-cursor","手机读过了，电脑为何又显示未读？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Fim\u002F01-message-state\u002F03-pagination-search","聊天搜索跳回旧消息后，怎样继续翻页？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Fim\u002F01-message-state\u002F04-unread-refresh","消息没停，未读角标就不更新？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F01-stream-parsing","一次接收回调，为什么不是一条消息？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F02-request-response","长连接上的请求，怎样等到自己的响应？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":43,"title":44,"meta":45},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F03-connection-lifecycle","登录一直在连接中，到底在等什么？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":47,"title":48,"meta":49},"\u002Fnotes\u002Fim\u002F03-message-content\u002F01-file-download","附件下载完了，为什么还是打不开？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":51,"title":52,"meta":53},"\u002Fnotes\u002Fim\u002F03-message-content\u002F02-image-send","图片上传到 100%，为什么还没发出去？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":55,"title":56,"meta":57},"\u002Fnotes\u002Fim\u002F03-message-content\u002F03-mentions","聊天 @提及怎样保存身份和位置？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":59,"title":60,"meta":61},"\u002Fnotes\u002Fim\u002F04-client-release\u002F01-flutter-build","Release 包为什么仍连着测试环境？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":63,"title":64,"meta":65},"\u002Fnotes\u002Fim","即时通信",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":67,"type":68,"sourcePath":69,"routePath":27,"path":27,"title":28,"description":70,"updatedAt":21,"meta":71,"body":79,"toc":202},"bm90ZTpub3Rlcy9pbS8wMS1tZXNzYWdlLXN0YXRlLzAzLXBhZ2luYXRpb24tc2VhcmNoLm1k","note","notes\u002Fim\u002F01-message-state\u002F03-pagination-search.md","Web 聊天列表既要向上翻历史，也要从搜索结果跳到某条旧消息。两个操作都在同一个列表中发生，取数方式却不同。",{"id":67,"type":68,"sourcePath":69,"slug":72,"title":28,"updatedAt":21,"description":70,"tags":73,"date":76,"comment":77,"summary":78,"lastUpdated":21},"im\u002F01-message-state\u002F03-pagination-search",[74,75],"frontend","IM","2026-07-27",true,"本地缓存、当前展示范围和同步进度分别管理。搜索围绕目标重建上下文，翻页再从窗口边界加载，避免把阅读位置当成同步位置。",{"type":80,"children":81},"root",[82,90,95,100,107,112,117,122,128,133,138,143,149,154,176,181,187,192,197],{"type":83,"tag":84,"props":85,"children":87},"element","h1",{"id":86},"聊天搜索跳回旧消息后怎样继续翻页",[88],{"type":89,"value":28},"text",{"type":83,"tag":91,"props":92,"children":93},"p",{},[94],{"type":89,"value":70},{"type":83,"tag":91,"props":96,"children":97},{},[98],{"type":89,"value":99},"项目把已获取的消息放在浏览器 IndexedDB 中，界面只展示其中一段，下面称为窗口。搜索把窗口移到很早的位置时，本地可能仍保留着最新消息，服务端同步也可能早已完成。",{"type":83,"tag":101,"props":102,"children":104},"h2",{"id":103},"为什么不能用窗口末尾继续同步",[105],{"type":89,"value":106},"为什么不能用窗口末尾继续同步？",{"type":83,"tag":91,"props":108,"children":109},{},[110],{"type":89,"value":111},"窗口回答“用户正在看哪里”，本地缓存回答“浏览器已拿到什么”，同步进度回答“服务端还有什么需要补”。把三者合成一个位置，搜索跳转就可能把同步起点一起拉回过去。",{"type":83,"tag":91,"props":113,"children":114},{},[115],{"type":89,"value":116},"解决思路是分别维护阅读窗口和同步边界。项目按会话范围从本地取消息，切换窗口不直接改变整个会话的同步进度。",{"type":83,"tag":91,"props":118,"children":119},{},[120],{"type":89,"value":121},"会话还带可选来源群，用来区分同一成员在不同群业务下的私聊。查询索引把会话、来源群和时间放在一起，避免先拿整个对象的数据，再让各调用方分别补过滤。正文在本地缓存中加密，外层索引字段仍可见，这也不能据此宣称消息已端到端加密。",{"type":83,"tag":101,"props":123,"children":125},{"id":124},"先显示本地再后台更新是不是更快",[126],{"type":89,"value":127},"先显示本地，再后台更新，是不是更快？",{"type":83,"tag":91,"props":129,"children":130},{},[131],{"type":89,"value":132},"可以这样设计，用户不用等网络；代价是远端校准回来时，需要合并变化并保住当前滚动位置。另一种方式是先读本地，再等校准完成或失败，统一提交窗口。",{"type":83,"tag":91,"props":134,"children":135},{},[136],{"type":89,"value":137},"项目当前使用后一种流程：先读本地分页，能构造历史请求时向服务端校准，新增数据写入本地后再读一次，最后更新窗口。远端失败则保留最初的本地结果。",{"type":83,"tag":91,"props":139,"children":140},{},[141],{"type":89,"value":142},"因此当前加载会把 HTTP 耗时算进去，尚未做到本地结果立即显示。以后改成后台补齐，还要处理用户已滚动或切会话的情况，不能让迟到结果把列表顶走。",{"type":83,"tag":101,"props":144,"children":146},{"id":145},"翻页用页码还是用消息边界",[147],{"type":89,"value":148},"翻页用页码，还是用消息边界？",{"type":83,"tag":91,"props":150,"children":151},{},[152],{"type":89,"value":153},"按页码取数比较直观，但聊天中会持续增加消息。按已知消息的边界继续取，更容易说明从哪里接上，也仍需要服务端定义返回顺序、边界是否包含和相同时间戳的处理。",{"type":83,"tag":91,"props":155,"children":156},{},[157,159,166,168,174],{"type":89,"value":158},"项目用 ",{"type":83,"tag":160,"props":161,"children":163},"code",{"className":162},[],[164],{"type":89,"value":165},"beforeOriginId",{"type":89,"value":167}," 取更早消息，用 ",{"type":83,"tag":160,"props":169,"children":171},{"className":170},[],[172],{"type":89,"value":173},"afterOriginId",{"type":89,"value":175}," 取更新消息。窗口合并时按消息 ID 去重，再按时间排序。去重能处理重叠，边界错误造成的遗漏却不能靠去重补回来。",{"type":83,"tag":91,"props":177,"children":178},{},[179],{"type":89,"value":180},"当前是否还有下一页，是根据本地重读数量或远端返回数量是否等于 limit 来估算。最后一批恰好满页时，会再允许查一次，得到空结果才确认结束。要提前明确到头，需要服务端返回游标或结束信息。",{"type":83,"tag":101,"props":182,"children":184},{"id":183},"搜索命中以后为什么要重建窗口",[185],{"type":89,"value":186},"搜索命中以后，为什么要重建窗口？",{"type":83,"tag":91,"props":188,"children":189},{},[190],{"type":89,"value":191},"普通翻页是在窗口边缘追加，搜索则要让用户看到目标消息前后发生了什么。只把命中的单条塞进旧列表，位置和上下文都可能不连贯。",{"type":83,"tag":91,"props":193,"children":194},{},[195],{"type":89,"value":196},"项目从本地读目标附近的上下文，默认大小为 10，替换当前窗口，再计算两个方向是否还能加载。目前这个入口只查本地，本地没有目标时，还不能自动去服务端补。",{"type":83,"tag":91,"props":198,"children":199},{},[200],{"type":89,"value":201},"搜索后下面有更多消息，只表示用户正在看较旧位置，不代表会话同步落后。",{"title":203,"searchDepth":204,"depth":204,"links":205},"",2,[206,207,208,209],{"id":103,"depth":204,"text":106},{"id":124,"depth":204,"text":127},{"id":145,"depth":204,"text":148},{"id":183,"depth":204,"text":186},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":211},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":213},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":215},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>"]