Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dbcb8e5
Create README_zh.md
VZqwq Jul 21, 2026
b3b1529
Update README.md
VZqwq Jul 21, 2026
d9dbcc4
Update README_zh.md
VZqwq Jul 21, 2026
26e35e2
Update README.md
VZqwq Jul 21, 2026
653c40e
Update README_zh.md
VZqwq Jul 21, 2026
7015593
Merge branch 'master' into master
VZqwq Aug 2, 2026
366b552
docs(openspec): keep expanded shell below viewport top
molvqingtai Aug 3, 2026
3bfaa3a
docs(openspec): preserve short viewport shell geometry
molvqingtai Aug 3, 2026
34ab956
fix(app-button): preserve expanded shell top inset
molvqingtai Aug 3, 2026
e3d0bc3
fix(content): unify shell and launcher geometry
molvqingtai Aug 3, 2026
5756662
refactor(content): isolate shell layout ownership
molvqingtai Aug 3, 2026
4ecc54f
docs(openspec): close shell top inset tasks
molvqingtai Aug 3, 2026
5f80d14
Merge pull request #98 from molvqingtai/fix/preserve-shell-top-inset
molvqingtai Aug 3, 2026
57f303f
docs(zh): 同步中文 README 至最新上游版本
VZqwq Aug 3, 2026
e8d8ae4
feat(content): highlight reactions and preview message images
molvqingtai Aug 3, 2026
10f3dcf
test(content): remove source-only contracts
molvqingtai Aug 3, 2026
4ebb9fe
docs(runtime): define concise untrusted-room error
molvqingtai Aug 3, 2026
e413379
docs(runtime): shorten connection timeout message
molvqingtai Aug 3, 2026
5f3ebd1
docs(runtime): avoid new tests for copy updates
molvqingtai Aug 3, 2026
231e330
fix(runtime): shorten error messages
molvqingtai Aug 3, 2026
09fb5fb
chore(integration): combine reviewed runtime messages
molvqingtai Aug 3, 2026
0f6d71d
docs(runtime): close copy change tasks
molvqingtai Aug 3, 2026
4081249
Merge pull request #97 from molvqingtai/feat/like-color-and-image-pre…
molvqingtai Aug 3, 2026
0a10eb5
Merge pull request #99 from molvqingtai/develop
molvqingtai Aug 3, 2026
b92b562
chore(release): 2.3.0 [skip ci]
semantic-release-bot Aug 3, 2026
1c68e07
Merge pull request #72 from VZService/master
molvqingtai Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# [2.3.0](https://github.com/molvqingtai/WebChat/compare/v2.2.0...v2.3.0) (2026-08-03)

### Bug Fixes

- **app-button:** preserve expanded shell top inset ([34ab956](https://github.com/molvqingtai/WebChat/commit/34ab956cfda7008c8e33fac2b7328c8a1386ecf9))
- **content:** unify shell and launcher geometry ([e3d0bc3](https://github.com/molvqingtai/WebChat/commit/e3d0bc32456e8e9e1417bb21a1b679bcfc54f0b6))
- **runtime:** shorten error messages ([231e330](https://github.com/molvqingtai/WebChat/commit/231e330c1ae640fd2d9bdaaba527270cd5b74ab6))

### Features

- **content:** highlight reactions and preview message images ([e8d8ae4](https://github.com/molvqingtai/WebChat/commit/e8d8ae426cff805400d1d6787406f2a1fae70558))

# [2.2.0](https://github.com/molvqingtai/WebChat/compare/v2.1.1...v2.2.0) (2026-08-01)

### Bug Fixes
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<img src="https://github.com/molvqingtai/WebChat/blob/master/public/logo.png" width="200" alt="WebChat logo" />
</p>

English | [简体中文](./README_zh.md)

# WebChat

[![CI](https://github.com/molvqingtai/WebChat/actions/workflows/ci.yml/badge.svg)](https://github.com/molvqingtai/WebChat/actions) [![GitHub License](https://img.shields.io/github/license/molvqingtai/WebChat)](https://github.com/molvqingtai/WebChat/blob/master/LICENSE) [![Chrome Web Store Version](https://img.shields.io/chrome-web-store/v/cpaedhbidlpnbdfegakhiamfpndhjpgf)](https://chromewebstore.google.com/detail/webchat/cpaedhbidlpnbdfegakhiamfpndhjpgf) [![GitHub Release](https://img.shields.io/github/v/release/molvqingtai/WebChat)](https://github.com/molvqingtai/WebChat/releases) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/molvqingtai/WebChat)
Expand Down Expand Up @@ -59,4 +61,4 @@ WebChat is an open-source project sustained by its contributors and sponsors.

## License

WebChat is available under the [MIT License](https://github.com/molvqingtai/WebChat/blob/master/LICENSE).
WebChat is available under the [MIT License](./LICENSE).
64 changes: 64 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<p align="center">
<img src="https://github.com/molvqingtai/WebChat/blob/master/public/logo.png" width="200" alt="WebChat logo" />
</p>

[English](./README.md) | 简体中文

# WebChat

[![持续集成](https://github.com/molvqingtai/WebChat/actions/workflows/ci.yml/badge.svg)](https://github.com/molvqingtai/WebChat/actions) [![GitHub 许可证](https://img.shields.io/github/license/molvqingtai/WebChat)](https://github.com/molvqingtai/WebChat/blob/master/LICENSE) [![Chrome 网上应用店版本](https://img.shields.io/chrome-web-store/v/cpaedhbidlpnbdfegakhiamfpndhjpgf)](https://chromewebstore.google.com/detail/webchat/cpaedhbidlpnbdfegakhiamfpndhjpgf) [![GitHub 发布](https://img.shields.io/github/v/release/molvqingtai/WebChat)](https://github.com/molvqingtai/WebChat/releases) [![询问 DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/molvqingtai/WebChat)

> 在任何网站上与任何人聊天。

WebChat 是一个去中心化的浏览器扩展,让你能与访问同一网站的人匿名聊天。它使用 WebRTC 进行端到端加密的点对点通信,因此没有中心化的聊天服务器,你的数据始终保留在自己的设备上。

将任意网站变成一个共享聊天室,并与已经身处其中的人们建立连接。

## 安装

<p align="center">
<a href="https://chromewebstore.google.com/detail/webchat/cpaedhbidlpnbdfegakhiamfpndhjpgf"><img src="https://img.shields.io/badge/Chrome-4285F4?style=for-the-badge&amp;logo=googlechrome&amp;logoColor=white" alt="Google Chrome" /></a>
<a href="https://microsoftedge.microsoft.com/addons/detail/mmfdplbomjjlgdffecapcpgjmhfhmiob"><img src="https://img.shields.io/badge/Edge-0078D7?style=for-the-badge&amp;logo=microsoftedge&amp;logoColor=white" alt="Microsoft Edge" /></a>
<a href="https://addons.mozilla.org/firefox/addon/webchat/"><img src="https://img.shields.io/badge/Firefox-FF7139?style=for-the-badge&amp;logo=firefoxbrowser&amp;logoColor=white" alt="Mozilla Firefox" /></a>
</p>

<p align="center">
<a href="https://chromewebstore.google.com/detail/webchat/cpaedhbidlpnbdfegakhiamfpndhjpgf">Chrome 网上应用店</a>
&nbsp;&middot;&nbsp;
<a href="https://microsoftedge.microsoft.com/addons/detail/mmfdplbomjjlgdffecapcpgjmhfhmiob">Microsoft Edge 扩展</a>
&nbsp;&middot;&nbsp;
<a href="https://addons.mozilla.org/firefox/addon/webchat/">Firefox 扩展</a>
</p>

## 使用方法

安装 WebChat 后,每个网站的右下角都会出现一个幽灵图标。点击它即可加入该网站的聊天室,与其他访客聊天。

## 演示

https://github.com/user-attachments/assets/e7ac9b8e-1b6c-43fb-8469-7a0a2c09d450

## 社区

加入 [WebChat Discord 社区](https://discord.com/channels/1398133810398367805/1398137562043908248),分享反馈、提出问题,并结识其他用户。

## 构建基础

WebChat 建立在以下优秀的开源项目之上:

- **[Remesh](https://github.com/remesh-js/remesh)** 提供了受 DDD 启发的状态模型,使应用逻辑独立于 React UI。
- **[shadcn/ui](https://ui.shadcn.com/)** 提供了可访问、可定制的 UI 基础组件。
- **[WXT](https://wxt.dev/)** 提供了跨浏览器扩展框架与构建工具。
- **[Comctx](https://github.com/molvqingtai/comctx)** 提供了扩展各 JavaScript 上下文之间的 RPC 通信能力。
- **[Artico](https://github.com/matallui/artico)** 提供了灵活的点对点通信 WebRTC 基础。
- **[ugly-avatar](https://github.com/txstc55/ugly-avatar)** 生成了 WebChat 独具特色的随机头像。

## 赞助商

WebChat 是一个由贡献者与赞助商共同维系的开源项目。

<a href="https://dartnode.com" title="由 DartNode 提供支持 - 面向开源的免费 VPS"><img src="https://dartnode.com/branding/DN-Open-Source-sm.png" width="500" alt="由 DartNode 提供支持" /></a>

## 许可证

WebChat 基于 [MIT 许可证](./LICENSE) 开放使用。
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
schema: spec-driven
created: 2026-08-02
goal: Unify positive reaction presentation and give every message image one stable,
bounded, zoomable preview path in the existing WebChat application root.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# highlight-likes-and-preview-message-images

Unify positive reaction color and add one stable, bounded, zoomable message-image preview path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
## Context

The message projection already exposes the current user's membership separately from the complete aggregate for both likes and hates. The shared `LikeButton` currently uses only current-user membership for each control's color, so either reaction contributed solely by other users retains the default gray presentation.

The shared Markdown renderer currently has two image branches: Markdown image syntax and image-valued links. Both use `max-width:70%`, neither has a height bound, and their duplicated element construction can diverge. The current App mounts AppMain, AppButton, and Danmaku inside one Shadow-root React tree. No image-preview owner exists.

See `proposal.md` for motivation and `specs/webrtc-runtime/spec.md` for the complete observable contract.

## Goals / Non-Goals

**Goals:**

- Keep current-user membership as each reaction control's toggle truth while deriving both like and hate color from their matching existing aggregates.
- Give both Markdown image forms one sanitized inline renderer and one preview activation path.
- Give each message image one lifecycle-owned Blob URL that its inline and preview images reuse.
- Size inline images from one message-content CSS query container, with equal `70cqi` maximums on both axes and no runtime geometry owner.
- Keep exactly one local `MediaPreview` component/state owner inside the existing App root and at the Danmaku component level.
- Provide deterministic fit, toolbar order, guarded opening, zoom, pan, layer, close, focus, and motion behavior without host-page ownership.
- Keep interaction controls keyboard-, pointer-, touch-, and reduced-motion-accessible.

**Non-Goals:**

- Changing reaction projection, LWW, count values, add-or-remove semantics, send commands, history, persistence, peer protocol, or message content.
- Adding another Domain, Extern, application root, portal root, event bus, persistence key, dependency, permission, public API, or host-page style owner.
- Adding any media type other than the currently confirmed rendered message image.
- Adding image upload, alternate network fetching, crop, rotation, download, carousel, annotation, square placeholders, runtime measurement, `ResizeObserver`, or persistent preview state.
- Mutating host-page business state, assigning transition names or styles to host elements, styling the document transition pseudo-tree, or adding an experimental/scoped transition API branch.
- Changing the WebChat shell, AppButton, Danmaku, notification, unread, Runtime, or page-scroll behavior.

## Decisions

### 1. Separate aggregate reaction emphasis from current-user toggle state

Each reaction control derives its active visual fact directly from whether its already projected aggregate count is greater than zero. A positive like selects the same red treatment for the like icon and count; a positive hate selects that same red treatment for the hate icon and count. Each existing current-user membership boolean remains the independent interaction state that decides whether the next command adds or removes only that user's matching reaction.

The shared reaction-button presentation owns this symmetric `count > 0` rule. Like count changes affect only the like control, and hate count changes affect only the hate control. No second projection, cached color state, Domain query, or reaction record is needed.

### 2. Render every message image through one shared control

Both ReactMarkdown image callbacks delegate to the same message-image component. The containing message content establishes `container-type: inline-size`. The image applies `max-inline-size: 70cqi`, `max-block-size: 70cqi`, `inline-size: auto`, `block-size: auto`, and `object-fit: contain`, then exposes one accessible preview trigger from the sanitized rendered source and alternative text.

That message-image component owns exactly one Blob URL for its current source. The inline image and the preview request reuse that URL; rerendering and activation do not create another URL. The same lifecycle owner revokes it exactly once when its image leaves the message lifecycle. This removes repeated source conversion without making browser image-decoding behavior part of the product contract.

The two equal maximums are derived from the same message-container inline size while the automatic used dimensions preserve the source aspect ratio. There is no fixed square box, crop, JavaScript measurement, `ResizeObserver`, measured-size state, or second sizing owner. This makes size, containment, focus, and activation structurally identical for both syntaxes.

### 3. Make `MediaPreview` the sole local preview owner in the existing root

One `MediaPreview` component owns the current image, activating element, zoom, translation, and in-progress gesture. It lives in the existing App React/Shadow tree at the same composition level as the Danmaku container and renders its overlay in place. A component-scoped React context exposes only the open-image action to the shared message-image renderer; it creates no second business owner, portal, document root, global event, Domain, or Extern.

The current contract accepts only the lifecycle-owned Blob URL and alternative text derived by the shared message-image renderer. The generic component name does not authorize any unconfirmed input type, speculative branch, placeholder, or fallback.

### 4. Split backdrop and preview-body layer ownership

The extension-owned application surface establishes one stacking context above host-page content. Inside it, the backdrop uses a local layer below the existing WebChat shell, AppButton, and Danmaku, while the preview body, image, and controls use a local layer above those surfaces. The icon toolbar is laid out below the preview image rather than above it or over it. The shell therefore remains visible and operable above the backdrop wherever the preview body does not cover it. The backdrop uses a neutral dark fill at exactly `18%` opacity and no blur, gradient, or decorative surface.

During a document View Transition, browser-owned snapshots render in the transition pseudo-layer above ordinary document stacking. That temporary transition placement is consistent with the preview body being above the shell. Layer tests bind the real application stacking context and relative order, not a duplicated root or an assumption about arbitrary host-page z-index values.

### 5. Derive one fitted baseline and one bounded transform

The preview first computes an aspect-preserving natural-size fit inside the viewport minus `24px` on each edge. The smaller of natural size and available size becomes fitted `1x`, so opening does not implicitly upscale a small image. Zoom is one multiplier over that baseline, clamped to `[0.25,4]`. User zoom may therefore reduce the image below its fitted baseline and natural dimensions without changing the initial open size.

For each axis, pan remains zero while the scaled image fits the available rectangle, so every reduced image stays centered. When it overflows, translation is clamped to half the difference between the scaled and available dimensions. An extreme pan therefore aligns, but never moves, the corresponding image edge past the viewport margin. Focal wheel and pinch zoom preserve the image point under the gesture before applying the same clamp. Viewport resize recomputes fit and clamps the current transform without persisting it.

### 6. Centralize pointer, touch, wheel, and keyboard interaction

The four icon controls use familiar zoom-out, zoom-in, reset, and close symbols with accessible names and tooltips. Button and keyboard zoom use `0.25x` steps; every zoom input shares the `0.25x` through `4x` bounds; reset and `0` restore fitted `1x`. The zoom-out control is disabled only at `0.25x`, and zoom-in is disabled at `4x`. Wheel/trackpad and two-pointer pinch use their local focal point. A single captured pointer pans only while zoom makes at least one axis overflow; reduced images that fit remain centered and do not pan.

The owner records whether a pointer sequence became a drag. Release settles pan and suppresses the click that would otherwise bubble to the backdrop. Event prevention is scoped to preview gestures; no handler mutates `document.body`, document scrolling, or host styles. Closing clears every gesture and transform fact.

### 7. Keep open admission, close, and focus settlement in one owner

The owner admits one open request only while it is closed and records the opening state synchronously. Every later open request while opening or open returns without changing the source, activator, transition, zoom, pan, or gesture state. There is no replacement operation.

Backdrop click without a drag, the close icon, Escape, and synchronized shell collapse call the same close operation. It clears the current image and transform exactly once, then restores focus to the saved activating element if it is still connected.

The shell stays operable above the backdrop in every area the preview body does not cover. Its ordinary message and control behavior remains independent except that collapsing the shell also closes its current preview.

### 8. Use one document View Transition with one temporary image identity

When `document.startViewTransition` is available and reduced motion is not requested, `MediaPreview` performs the admitted open or close state operation inside one document View Transition. The activating image and current preview image share one generation-scoped temporary identity, while the browser also captures and briefly crossfades the document root. A pre-existing host-defined named participant remains browser-owned and may also participate. That document participation is visual only: WebChat assigns no name or style to a host element and changes no host business state.

The sole preview owner keeps one active temporary-identity record and restores the original identity when the operation settles or closes. A repeated open cannot supersede that record. Preview state remains authoritative, and the admitted operation settles exactly once without another preview or stale marker. Reduced motion, a missing API, a synchronous failure, a rejected or skipped transition, or another document transition that prevents execution takes the same state operation immediately. Motion never delays close cleanup or focus restoration.

### 9. Verify behavior through current UI boundaries

Focused component controls cover positive and zero aggregates for both likes and hates, type-local color independence, and current-user add/remove behavior for each control. Shared renderer controls cover both Markdown syntaxes, the message-content query owner, equal `70cqi` maximums, automatic aspect-preserving dimensions, one Blob URL per image lifecycle, inline/preview URL identity, one-time revocation, sanitized source/alt preservation, keyboard activation, and the absence of runtime measurement state. `MediaPreview` controls cover composition ancestry, one owner, the split backdrop/body layers, toolbar placement below the image, `24px` fit, no implicit upscale, opening/open repeated-request no-op behavior, every close path, focus, zoom inputs, focal math, pan bounds, drag suppression, reset, resize, event cleanup, document View Transition, whole-page crossfade, reduced motion, failure fallback, and host-state preservation.

Browser-mode coverage verifies rendered geometry, input behavior, focus, and computed layer order. Structural controls exclude a second root/portal, Domain/Extern/persistence/dependency, duplicated image policy, and host `body` mutation.

## Risks / Trade-offs

- [A red reaction no longer means the current user personally contributed it] -> Current-user membership remains the independent toggle input and accessible pressed state for each reaction; red intentionally represents any positive matching aggregate.
- [The preview body can overlap the WebChat shell] -> This is the confirmed layer result; the preview body wins above the shell while the shell remains visible and operable above the backdrop wherever it is not covered.
- [Zoom gestures can leak into the host page] -> Consume only active preview wheel/pointer/touch gestures with non-passive handling where required, and never install a body scroll lock.
- [A pan release can look like a backdrop click] -> Track drag intent under pointer capture and suppress only that settlement click.
- [Document View Transition crossfades the page root] -> The brief whole-page and shell crossfade is accepted for open and close; final layout, color, layer, and business state remain unchanged.
- [A later image activation can arrive while opening or open] -> The sole owner records opening synchronously and treats every later open request as a no-op, so no second source, transition, or transform state can compete.
- [Another document transition can prevent execution] -> The unavailable, skipped, rejected, or blocked path applies the admitted preview state directly.
Loading
Loading