Compare commits

..
66 Commits
Author SHA1 Message Date
Your Name d2b4cb0440 ci: deploy pro with docker compose
Deploy im-webapp production / deploy (push) Successful in 1m23s
2026-10-18 20:11:07 +08:00
richjohnrambo 72c4f4e54d fix: defer revoking group file download URL 2026-10-17 17:09:06 +08:00
richjohnrambo dd29eaca42 fix(web): sync group member nicknames 2026-10-17 10:12:20 +08:00
richjohnrambo 8bdb8ed5a5 feat(web): show legal documents in registration dialogs 2026-10-17 01:30:03 +08:00
richjohnrambo dca34b8b5d fix(web): open legal documents on IM API host 2026-10-17 01:23:41 +08:00
richjohnrambo c30c5c07c7 feat(web): add tenant user registration flow 2026-10-17 00:48:08 +08:00
richjohnrambo 361e23b9ec fix p2p conversation avatars 2026-10-14 16:09:38 +08:00
richjohnrambo 8a26a4ccf9 fix production attachment file origin 2026-10-14 15:22:46 +08:00
richjohnrambo c1338985d4 chore: remove unused service worker cleanup 2026-10-14 14:24:37 +08:00
richjohnrambo 440e29975a chore: remove unused web code and dependencies 2026-10-14 14:10:41 +08:00
richjohnrambo b7f214daf5 remove legacy root Tinode webapp 2026-10-14 14:01:52 +08:00
richjohnrambo ec22b709b5 fix legacy attachment URLs in Vite web 2026-10-14 13:58:33 +08:00
richjohnrambo 0570f2c5bd fix legacy chat attachment URLs 2026-10-14 13:38:01 +08:00
Your Name dd77ba903b Queue message sound while audio is priming 2026-09-30 17:05:32 +08:00
Your Name 06df51ca08 Prime message audio and silence duplicate OS notification sound 2026-09-30 17:01:34 +08:00
Your Name 1c17165c86 Brand web client and add message notification sound 2026-09-30 16:50:14 +08:00
Your Name 560269c299 docs: plan web branding and message sound 2026-09-30 16:41:39 +08:00
Your Name 7b8216ea70 fix(web): prevent negative unread counts 2026-09-30 15:58:34 +08:00
Your Name bd61be8a86 feat(web): support group avatar updates 2026-09-30 15:28:50 +08:00
Your Name cdd46c0b62 docs: plan web clipboard image compatibility 2026-09-29 17:37:38 +08:00
Your Name 96acc6cb39 docs: design web clipboard image paste compatibility 2026-09-29 17:32:40 +08:00
Your NameandClaude Sonnet 5 975dc77111 Revert "网页版支持粘贴/选择多张图片暂存,可配文字一起发送"
用户反馈这个交互做得不够好,要求退回上一版(保留粘贴发图/保存图片,去掉多图暂存+图文一起发)。

This reverts commit 6bcfc08943.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:12:29 +08:00
Your NameandClaude Sonnet 5 6bcfc08943 网页版支持粘贴/选择多张图片暂存,可配文字一起发送
老板反馈:粘贴图片、选择文件都是立即发送,体验不好;想要能粘贴多张图片,并且可以带着文字一起发,
不带文字也要能发。

- 粘贴图片、点击图片图标选图(新增 multiple 多选)现在都不再立即发送,改成暂存进输入框上方的
  缩略图预览条,可以继续粘贴/选择叠加更多图片,每张都能单独点掉重新移除
- 发送(Enter / 点击发送按钮)时,把暂存的图片按添加顺序依次发出去,再发文字(如果有输入的话);
  没有文字、只有图片也能发;没有图片、只有文字保持原有行为不变
- 引用回复只附着在这一批里实际发出的第一条消息上,跟"引用只作用于下一条消息"的既有规则保持一致
- 单张图片发送失败不影响这一批里的其他图片和后面的文字,各自独立报错提示
- 参考设计图(语音图片发送状态.png)确认了这个后端每种附件类型都是独立气泡,没有"图文合并成一条
  消息"的协议形态,所以是按多条消息依次发送实现的,不是改协议

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:06:24 +08:00
Your NameandClaude Sonnet 5 3d48abdf73 网页版支持粘贴图片发送、聊天图片保存到本地
老板反馈:"没有复制粘贴图片功能,只能保存在电脑里面发,图片也没有保存功能"

- 聊天输入框新增粘贴图片检测:剪贴板里有图片(截图工具/复制图片)时直接走发图逻辑,
  不再要求先存到电脑里再用文件选择器上传;普通文本粘贴不受影响
- "查看大图"预览里新增"保存图片"下载按钮,取消息自带的原始文件名,
  取不到时按mime类型生成默认文件名

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:42:21 +08:00
Your NameandClaude Sonnet 5 9abae5030b 修复codereview复核确认的多选/转发/收藏页/资料页等前端bug
- 多选模式下Drafty链接、名片消息不再意外触发跳转/开新标签页
- 语音/视频消息在多选模式下点击可正常冒泡选中该行
- 转发排除未确认/发送失败消息,识别uncertain转发结果并单独提示
- 群文件"更多操作"菜单竞态、群成员移出状态竞态
- 收藏页链接卡片new URL()崩溃点加防御,并给登录后应用区域加错误边界
- 个人资料页乐观锁版本冲突时清空草稿避免静默覆盖并发修改

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 16:47:32 +08:00
Your NameandClaude Sonnet 5 e3eed8bdd4 Add click-to-view lightbox with zoom for chat images
Tapping a chat image opens a full-screen viewer (same resolved
image source, no extra fetch) with zoom in/out buttons, mouse-wheel
zoom, drag-to-pan once zoomed, and double-click to toggle zoom.
Wheel zoom uses a native non-passive listener rather than JSX onWheel,
since a passive listener's preventDefault() is silently ignored and
would leave the page scrolling under the lightbox.

Also closes a gap the row-click-to-select change (1cc6f506) left open:
image/video/audio/file attachments inside a bubble had no selectMode
awareness, so tapping one during bulk-select would also open the
lightbox / play media / start a download alongside toggling the row.
Added a `locked` prop to all four attachment components (disabling
their own click behavior without changing how they look) and wired
`locked={selectMode}` from both MessageBubble.tsx and drafty.tsx's
IM/EX/AU/VD cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 22:32:07 +08:00
Your NameandClaude Sonnet 5 aca5f8609b Fix misleading "clear chat history" confirmation copy
The dialog said clearing was "local record only" -- reading the real
backend handler (im_be's Topic.replyDelMsg) shows a soft delete
persists a per-account delete watermark server-side, visible across
every device/session on that account and not reversible through any
normal client action. Only the "doesn't affect the other party" half
of the old copy was accurate. Updated both the P2P and group versions
to say what actually happens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 22:21:56 +08:00
Your NameandClaude Sonnet 5 1cc6f5068a Make whole message row tappable to toggle selection
The selection checkbox was the only clickable target in multi-select
mode; tapping the rest of the row (bubble, avatar, timestamp) did
nothing. Moves the toggle handler to the row container so tapping
anywhere selects/deselects, matching the multi-select UX in every
other list this app has (favorites, blocklist, etc).

Nested controls that keep their own click behavior during selectMode
(retry-failed-send, @mention profile card, quote-bar jump-to-message)
would otherwise double-fire alongside the new row click -- retry stops
propagation, mentions and the quote bar go inert instead (same
treatment this file already gives the avatar/sender-name buttons
during selectMode), so a tap only ever does the one thing: toggle the
row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 13:42:27 +08:00
Your NameandClaude Sonnet 5 95c0677b55 Fix forward comment landing before forwarded content
The comment send was fired in parallel with the forward REST call,
not chained after it -- confirmed live with a 2-source bulk forward,
where the comment (plain WS publish, fast) landed before the forwarded
messages (async endpoint, responds with status:"pending" before the
messages are actually persisted server-side). Chains the comment send
off the forward promise instead of starting both at once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:37:01 +08:00
Your NameandClaude Sonnet 5 4cd2b33c53 Migrate message forwarding to backend REST endpoint
Both real mobile clients (iOS, Android) forward messages via POST
/v1/im/messages/forward, never a client-side {pub} packet -- confirmed
by reading their source directly. Switches web's single-message and
bulk forward paths to the same endpoint, which copies message content
verbatim regardless of shape, eliminating the client-side flat-content
detection this used to require. Comment-sending stays on the existing
sendText() path, unchanged, to preserve current ordering.

Removes send.ts's now-unused forwardMessage/flatForwardPayload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:27:37 +08:00
Your NameandClaude Sonnet 5 e7cb4edf3a 读了iOS/Android真实源码后修正:busy确实是真实的通话状态,之前判断错了
早前(Part I/K)判断"busy从未在持久化消息里真实出现过,只是本应用自己在
live信令上用的值",所以没把它并入 CallWebrtcState,只在本地 UI 状态单独
加了个 union。这次拿到手机端真实源码核对,iOS(ServerMessages.swift的7态
枚举)和Android(MsgServerData.java的BUSY)都把busy当成服务端下发的正式
head.webrtc取值之一,和accepted/finished等其余几个是同一级别的东西,
不是本应用凭空发明的——之前的判断依据不足。

补上 case 'busy' 的通话结果文案("对方忙线中",和已有的实时来电忙线提示
用词一致),去掉 stores/calls.ts 里两处不再需要的 `| 'busy'` 单独union。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 11:51:28 +08:00
Your NameandClaude Sonnet 5 1d383b4e93 转发消息的来源标记改用手机端同款字段,修复网页转发在手机端认不出"转发"的问题
网页版转发功能此前一直用自造的 head.forwarded: "topic:seq" 单字符串字段记录
来源,且这个字段在整个网页项目里从未被任何地方读取过(纯写入,无消费者)。

数据库排查(排查转发内容丢失bug时顺带确认)发现手机端自己的转发用的是完全
不同的字段:head['x-im-forwarded']:true + head['x-im-forward-source']:
{seq,from,topic},是真实双端数据。网页版这个自造格式手机端根本不认识,
意味着从网页转发出去的消息,即使内容显示正常,手机端也大概率认不出"这是
一条转发消息",不会显示对应的转发来源提示(如果手机端UI有这个功能的话)。

改成手机端同款的两字段格式。确认这不影响网页自己的任何逻辑(旧字段确认无
读取方)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 11:34:44 +08:00
Your NameandClaude Sonnet 5 ed2920275d 修复资料弹窗点击自己头像必定加载失败
GET /users/{id}/profile 对自己的id恒定400(和更早修过的群内自己资料
400是同一类self-lookup限制),点击群成员格子/列表里自己的头像会一直
显示"加载失败,请重试"。改成自己走专门的 GET /me/profile(没有这个
限制),归一化成和他人资料相同的字段结构供同一套渲染逻辑使用。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 00:49:06 +08:00
Your NameandClaude Sonnet 5 0ba5c97d78 codereview修复:资料弹窗好友关系判断、转发内容克隆、账号切换缓存残留等
三路独立codereview(群成员UI+资料弹窗 / 未读红点+转发修复 / 搜索+复制修复)
发现并修复:

- UserCardRoot 资料弹窗此前完全没读 profile.relationship,对陌生人也直接
  显示"发消息"/通话按钮——现在按好友/待验证/对方已申请/不可添加/添加好友
  五态分支,和 FriendProfileView 保持一致,接入 AddFriendModal。
- useEscapeKey 注册位置错误:挂在常驻的 UserCardRoot 而不是随卡片开关的
  UserCardPanel,导致从其他弹层里打开资料卡片时,按 Escape 关掉的是底下
  那层弹层而不是卡片本身。
- "移出"格子的可见性此前只判断 isAdmin,在"群主+多个管理员、没有普通成员"
  的群里会显示格子但点进去一个能移出的都没有——改成同时判断成员列表里
  是否真的存在能移出的对象。
- flatForwardPayload 对图片/文件/语音/视频/名片/通话结果类消息,转发时直接
  返回了原消息的 content 对象引用而不是拷贝——SDK 出站前会就地删除假值
  字段,等于转发的同时悄悄改写了原消息在本地缓存里的内容。改成每次转发都
  返回浅拷贝;顺手让检查顺序和其余识别这几种格式的地方保持一致(先判类型
  明确的附件/名片/通话结果,再判无类型标记的纯文本),并补上和
  mobileAttachmentContentOf/contactCardContentOf 同样的字段合法性校验。
- confirmedReadSeq 等聊天列表相关的模块级缓存此前从未在登出/切换企业时清
  空——同一个标签页不刷新页面直接换账号登录,会把上一个账号的"已读到哪"
  当成新账号的下限,把新账号真实未读的自聊消息误判为已读。补了统一的
  reset监听机制(connection.ts 的 resetTinode 现在会广播),topics.ts 据此
  清空所有相关缓存。
- 复制功能补final `?? ''`兜底,防止附件数据本身不完整时仍复制出"undefined"。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 00:45:34 +08:00
Your NameandClaude Sonnet 5 105ff76959 排查转发bug同类问题:修复查找聊天记录崩溃 + 复制功能复制出"undefined"
审查了所有"读取消息content做数据处理"的地方,找到两处和转发bug同一根因
(手机端扁平非Drafty内容格式)的真实问题:

1. 查找聊天记录(HistorySearchModal)的搜索结果渲染,对手机端发送的图片/文件/
   语音/视频/名片消息命中关键词时会让整个应用崩溃——Drafty.toPlainText在这
   类扁平内容上返回undefined,highlightMatches对undefined调用.toLowerCase()
   直接抛异常,而这个应用完全没有错误边界(ErrorBoundary)兜底,等于白屏。
   已用真实搜索接口返回确认snapshot里其实带着head字段(此前SearchHit类型
   没建模),据此在渲染前识别这几种扁平内容并显示[图片]/[文件]/[语音]/[名片]
   这样的占位文案,不再无脑喂给renderDrafty;顺手给highlightMatches补了防御
   性判空,避免类似疏漏再次崩溃整个应用。

2. 消息右键菜单"复制"功能,对手机端发送的扁平文本消息(如群公告自动发布的
   那条)和图片/文件/语音/视频消息,复制到剪贴板的是字面字符串"undefined"。
   文本类修成正确提取真实文本;附件类直接从"复制"这个操作里去掉(参考名片/
   通话记录消息已有的排除逻辑),复制一段JSON式的附件数据本来就没有意义。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 00:06:10 +08:00
Your NameandClaude Sonnet 5 eb0a6eba7e 修复转发含flat非Drafty内容的消息(手机端文本/图片/语音/视频/名片/通话结果)导致content丢失显示"已删除"
真实数据库排查确认根因:forwardMessage()原来无条件把 msg.content 喂给
Drafty.forwardedContent(toDraftyContent(...)),假设内容永远是纯字符串或真
Drafty文档({txt,fmt,ent})。但手机端把文本/图片/语音/视频/名片都发成扁平非
Drafty JSON(如 {"text":"11"}),只靠 head['x-im-type']/head.webrtc 标记类型
——对这类对象跑 Drafty 处理会退化成空内容,SDK 出站的 simplify() 又会把空
content 整个从包里剥掉,服务端收到的 {pub} 根本没有 content 字段,落库后
content/content_nonce 都是真正的 NULL,客户端据此正确地(从自己的角度)把
它当成"已撤回"渲染成"⚠ 消息已删除"。

用 SSH+kubectl exec 直接查了k3s上的真实MySQL(meeting_im库, im_003_group_
messages_202609/im_003_messages两张表),确认了具体是哪几条消息、它们的
head/content 真实长什么样,而不是猜测。

修复:flatForwardPayload() 识别这几种扁平内容形状,文本类重建成真正的
Drafty文本,附件/名片/通话结果类原样保留内容和 x-im-type/webrtc 标记(接收
端已有的对应dispatch逻辑本来就认识这些形状),只有真正的纯字符串/Drafty
内容才继续走原来的 Drafty.forwardedContent() 路径。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 20:59:37 +08:00
Your NameandClaude Sonnet 5 0ace395cfb 修复群聊自己发消息后聊天列表出现虚假未读红点
reconcileMeSubs() 的周期性 me.getMeta(withLaterSub()) 拉取,对已知会话原有的
回归保护(seq<=prev.seq && unread>prev.unread) 只防得住"seq没变但unread变大"
这一种情形。自己在已打开的会话里发消息时:本地 confirmedReadSeq 会同步立即
更新(早于服务端),但发给服务端的 note read 是异步的;如果这次拉取的请求
恰好在发送前就已经在途,返回时 seq 已经真实前进(不满足 seq<=prev.seq)、
read 却还没追上,旧保护不生效,虚假的 unread 就被原样套用——哪怕这条消息
就是自己刚发的。

补一个独立的、无条件的兜底:用已有的 confirmedReadSeq 记录做下限,read 永远
不会被这次拉取拉回到低于"本客户端已确认看过的位置",与原有保护并存、不冲突。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 18:02:25 +08:00
Your NameandClaude Sonnet 5 ef31eb2e88 新增点击头像/昵称查看用户资料弹窗,接入消息气泡/@提及/群成员列表
新增全局 UserCardRoot(stores/userCard.ts 驱动,挂载在 AppShell 里,模式同
ConfirmDialogRoot),复用 getUserProfile 数据源,展示头像/昵称/性别地区/签名,
非本人时带发消息/语音/视频快捷操作,底部"查看完整资料"跳转到完整的
/contacts/{id} 页面。

接入点:
- MessageBubble.tsx:群聊和单聊的对方头像/昵称(原来只有群聊头像/昵称是
  Link 到完整资料页,单聊完全没有交互)
- drafty.tsx:@提及实体(Drafty.mention 的 uid 存在 ent[].data.val 里,
  之前渲染完全没读这个字段),"所有人"哨兵值除外
- GroupMembersModal.tsx / GroupDetailColumn.tsx:群成员列表的行内和紧凑
  头像格子

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 17:45:23 +08:00
Your NameandClaude Sonnet 5 2427a3782c 群成员格子改回独立"移出"格子,点击打开成员列表,不再是逐头像角标+直接移出
用户再次纠正:不要在每个可移出成员的头像上单独加角标、点击直接弹确认移出;而是
像"+邀请"一样,做一个独立的"-移出"格子(同款虚线方块,仅 isAdmin 可见),点击后
打开"查看全部"成员列表,真正的移出操作留在列表里已有的那一行按钮上。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 17:28:17 +08:00
Your NameandClaude Sonnet 5 05ee23aefa 群成员格子改用头像外角标 ⊖ 移出,替换上一版的行内文字按钮
上一版把 ➖ 加在了"查看全部"弹层的成员行文字按钮里,用户纠正:应该是紧凑头像格子
(群聊设置面板里"查看全部"之前那一屏)里,像"+邀请"那样在头像外侧显示一个独立的
角标,而不是列表行内的文字。canRemoveMember 挪到 lib/api/groups.ts 共享,避免
两处判断逻辑分叉。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 17:19:45 +08:00
Your NameandClaude Sonnet 5 2d589cbe6b 群成员移出按钮补充 remove_circle 图标
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 16:58:34 +08:00
Your NameandClaude Sonnet 5 304283c8d8 fix(messages): @mentions leaked the sender's private remark
Reported + reproduced live: a group owner's own private friend remark
for a member ("小号1"/"私密备注勿外传" in testing) ended up baked into
that member's @mention text, visible to every other group member with
no idea what it referred to.

Root cause confirmed by reading server/im_http.go's userSummaryPayload:
GET .../members?mentionable=true's `display_name` unconditionally
prefers the CALLING viewer's own remark for that member when one is
set. That's correct for a list only the viewer sees, but MentionPicker
was using it to build the mention's label, which gets baked into the
message's shared Drafty content once (mentions.ts's buildMentionedContent)
and read identically by everyone from then on -- unlike a sender-name
label, which every viewer's own client re-renders fresh per message and
can legitimately show their own remark for.

Switches MentionPicker's label (both the picker's own visible list and
the mention baked into the sent message) to the same response's
`nickname` field (copyUserNickname in the same backend function),
which reads straight off the member's public profile and is never
remark-shadowed. Verified live: set a real friend remark, confirmed
the picker still showed the real nickname (not the remark), picked it,
and confirmed the actually-sent message's @mention text was the real
nickname too -- while the unrelated sender-name label elsewhere in the
same conversation correctly kept showing the remark, confirming the
fix didn't touch that intentionally-different behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 16:10:03 +08:00
Austin cbb01085c4 fix(reply): 修复回复消息中媒体引用的兼容性问题
适配了 Drafty 媒体的 `refurl` 字段,同时兼容旧的 `ref` 和 `val` 字段,确保不同来源的媒体链接都能正确解析用于渲染。
2026-09-23 21:37:07 +08:00
Austin a8d3504424 feat: 实现跨端引用回复协议,新增 QuoteBar 组件支持消息引用
本次提交实现了与 iOS 对齐的跨端消息引用协议,新增 QuoteBar 组件用于渲染引用消息栏,重构发送逻辑支持文本和媒体消息的引用功能。主要改动包括:
- 新增 reply.ts 处理引用数据的构建和解析
- 新增 QuoteBar 组件渲染引用消息预览
- 重构发送逻辑,将引用快照放入消息头部而非内容
- 调整代理配置指向本地开发环境
- 优化消息操作权限,支持名片和媒体消息引用
2026-09-23 20:40:47 +08:00
Your NameandClaude Sonnet 5 fb92f4f703 fix(groups): hover affordance on the remove-member button
Reported: the new "移出" (remove member) button gave no visual
feedback on hover, so it didn't read as clickable. Tailwind's own
preflight reset sets buttons to cursor:default (not the browser's
usual pointer) unless a cursor utility says otherwise -- adds
cursor-pointer + hover:underline, verified live via
getComputedStyle/:hover that both apply while hovering and correctly
reset on the disabled (in-flight) state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:27:17 +08:00
Your NameandClaude Sonnet 5 4961c0ae6e fix(groups,system): remove-member action + stale invite button
Adds removeGroupMember (DELETE /groups/{topic}/members/{user_id}) --
confirmed live the backend now has this route (server/im_http.go's
serveIMGroupMember + canRemoveIMGroupMember), which an earlier pass
recorded as a genuine protocol gap; it isn't anymore. Wired into
GroupMembersModal with a per-row "移出" button gated by the same rule
the backend enforces (owner or admin removes a plain member, only the
owner removes an admin, nobody removes the owner or themselves), with
a confirm dialog before the real DELETE call. Verified live end-to-end
against a real owned group: member count 4 -> 3, list updates
correctly (the side panel's own separate "群成员 N" summary count
still trailed behind after this -- confirmed via a direct GET that the
server's own group payload hadn't recalculated member_count yet, a
backend read-consistency lag unrelated to this fix, not chased
further).

Also fixes the reported "已处理的群邀请消息,即使群解散了,接受邀请
按钮还是一直可以点" bug: the button's visibility used to be derived
purely from the notification's static action payload every render, so
neither a successful accept nor a definitively-failed one (expired,
already handled, group gone) ever stopped it from looking freshly
clickable again. Tracks per-invitation resolved state (accepted/
failed) once either outcome happens and swaps the button for a static
resolved label. Doesn't survive a page reload for invitations resolved
in an earlier visit (the notification record itself carries no
resolved flag to read back) -- a real, acknowledged limit, not
silently hidden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:08:07 +08:00
Your NameandClaude Sonnet 5 6a65998e42 fix(messages): render mobile-sent image/file/voice/video attachments
Live-confirmed in a real group ("seq测试", tenant 10001) that image
messages sent from Android and iOS render as completely blank bubbles
on web, and the chat list's own preview for one showed the literal
string "undefined". Root cause confirmed by querying the actual
im_messages/im_group_messages rows: mobile apps send image/file/voice/
video as a flat, non-Drafty content object (`{file_id,
thumbnail_file_id, mime, width, height, name, size, ref?, preref?}`)
tagged only by `head['x-im-type']`, not as a real Drafty `{txt,fmt,ent}`
document the way this web client's own sender (attachments.ts) always
builds. renderDrafty/formatMessagePreview had no case for this shape
and silently produced nothing / "undefined" -- the exact same failure
pattern already fixed once for `contact_card` (see dispatch.ts's
existing contactCardContentOf), just not yet extended to attachments.

Adds mobileAttachmentContentOf (dispatch.ts), normalizing `ref`/
`preref` from `file_id`/`thumbnail_file_id` when the wire message
omits them (confirmed both cases occur on real messages), and wires it
into MessageBubble.tsx (reusing the existing Image/File/Audio/Video
Attachment components) and preview.ts (matching bracket text). Extends
VideoAttachment with a remote `previewRef` fallback alongside its
existing inline-base64 `preview`, since mobile videos' poster is a
separately-hosted file, not inline data.

Verified live against the real message rows: detection and URL
construction are confirmed correct (network panel shows the real OSS-
backed URL, exactly matching the file_id from the DB row); the actual
pixels don't load in this local dev environment because OSS's CORS
whitelist doesn't include localhost, a known, already-documented
limitation of testing via localhost instead of a real *.118im.com
domain, not a defect in this fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:48:43 +08:00
Austin 0c38adba78 fix: 修复登录时因租户不一致导致的422策略违规错误
在连接建立时记录服务器实际使用的租户代码,而非直接从存储中读取,避免登录时的租户类型判断与会话租户不一致导致的TENANT_CODE_FORBIDDEN错误。新增登录失败后的自修复重试逻辑,当收到422错误且原因是租户代码被禁止时,临时覆盖登录类型并重试一次,同时重置相关状态避免影响后续连接。
2026-09-22 20:17:39 +08:00
Your NameandClaude Sonnet 5 370bf9d5b0 fix(tinode): trim tenant code before login_type comparison
Code review of the login_type fix (11276804) found tenantCode.toLowerCase()
=== 'default' doesn't trim, so a tenant code with stray leading/trailing
whitespace would be misclassified as enterprise (login_type:0) instead of
personal (login_type:1). tenant.code always comes from the resolve
response, not raw user input, so this was a latent edge case rather than
a reproduced bug -- hardened anyway since the fix is free, matching the
.trim() already used on tenant code elsewhere in this codebase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:42:39 +08:00
Your NameandClaude Sonnet 5 1127680425 fix(tinode): send login_type on basic-scheme login (Zentao bug #33)
server/user.go's validateLoginTenant now hard-requires a login_type
field (0 enterprise / 1 personal) on every {login scheme:"basic"}
message, rejecting the rest with a 422 LOGIN_TYPE_REQUIRED policy
violation -- confirmed live by reading the currently-deployed
server/session.go call site and server/user.go's validation logic.
The vendored tinode-sdk has no API surface for this custom field (its
login()/loginBasic() packet builders are private class fields that
only ever write scheme/secret/cred), so every basic-scheme web login
was failing.

Extends the existing installTenantHiPatch wire-level interception
(same technique already used for {hi}.tenant) to also inject
login_type into outgoing {login} packets, derived from the resolved
tenant's code. tenant_code is deliberately left unset: the backend
falls back to the session's own tenant (already established via the
{hi} patch) when the {login} message omits it, for both the personal
and enterprise branches. token-scheme logins (session restore/
reconnect re-auth) are untouched.

Verified live: captured the actual outgoing WS frame now carrying
login_type, and confirmed the resulting error changed from what would
be a 422 policy violation to a normal credential-mismatch error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:20:08 +08:00
Your NameandClaude Sonnet 5 ed029c59ef fix(tinode): auto-recover from cluster-failover topic proxy teardown
Native Cluster failover tears down a topic's server-side proxy session
on the surviving nodes without notifying the client -- the SDK's local
_attached flag is never reset on this path, so isSubscribed()/
ensureTopicAttached() keep reading a stale true and can't catch it in
advance. The server replies 409 "must attach first" to the {pub}
itself; publishWithReattach() catches exactly that, forces a fresh
subscribe(), and retries the same msg object once (safe: publishDraft's
own _noForwarding guard prevents a duplicate local echo, and the
unchanged x-im-client-mid still dedupes server-side). Wired into all 9
publishDraft call sites (send.ts, attachments.ts, calls.ts).

Confirmed live on an independent native-Cluster verification
deployment: killing the leader pod mid-conversation previously caused
every send to fail until a manual resubscribe; with this fix, 36/36
probes across the same failover succeeded with no manual intervention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:44:39 +08:00
Your NameandClaude Sonnet 5 74351af905 fix(messages): merge stale-sender-profile fallback into #32's uid-shape guard
Two independent fixes for the same raw-uid-leak bug landed in parallel:
upstream added groupSenderLabel() to catch uid-shaped names by regex
(covers a still-present member whose display_name IS the uid); the local
change instead resolved a left-group sender's real name via a standalone
getUserProfile() lookup (covers a member no longer in memberByUserId at
all). Neither alone covers both paths. groupSenderLabel now takes multiple
candidates in priority order -- member-list name first, then the fetched
stale-sender name, falling through to the placeholder only when neither
resolves to something that isn't uid-shaped -- so a left member with a
resolvable profile still shows their real name instead of the placeholder,
while the still-present-but-nameless case is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 13:50:41 +08:00
Austin ca7af8baf9 feat(acc-support): 实现动态获取并渲染联系我们条目
新增从后台获取管理员配置的联系我们数据的功能,支持邮箱和电话的跳转链接, fallback 到旧的固定链接配置
2026-09-21 13:12:56 +08:00
Austin 4dadb1bd5a fix(tinode/errors): 处理429账号锁定错误的提示信息
清理429错误消息末尾的状态码后缀,直接使用服务器返回的中文提示,无匹配时使用备用文案
2026-09-20 17:08:59 +08:00
Austin 1952f0ebcd fix(messages): never render raw uid as group sender name (#32)
Senders missing from the member list (left/removed) fell back to msg.from verbatim, and nameless members arrive from the REST API with the uid itself as display_name. Resolve both through a shared groupSenderLabel helper that substitutes a fixed placeholder for uid-shaped names (message bubbles, quoted replies, history search).
2026-09-20 16:42:35 +08:00
Austin 41728017cb Merge branch 'dev' of https://git.118im.com/im_team/im_webapp into dev 2026-09-18 18:34:21 +08:00
Your NameandClaude Sonnet 5 7cf8e4f4e0 fix(groups): correct rename-sync event name and unreliable getMeta path
The previous fix listened for a documented-but-wrong event name
(group.profile.updated); live instrumentation of a real rename showed
the actual wire event is group.settings.updated. Fixing only the name
still didn't work: the refresh mechanism it triggered
(getMeta(startMetaQuery().withDesc().build())) was itself unreliable
against this backend -- live-confirmed to either time out (504) on a
brand-new topic or return stale pre-rename data on an established one,
which also explains the "second rename flips back to the original
name" symptom (a late, stale response from an earlier call clobbering
the correct value).

Replace it with the already-reliable REST getGroup() response applied
locally via the SDK's own _processMetaDesc (same internal method the
SDK calls when a real meta response arrives; confirmed to reach
me.contacts(), which the sidebar/header read).

Also fixes a second, independently-discovered bug found while
verifying the above: notifyTopic's requestAnimationFrame-based
notification batching could get permanently stuck if the callback
was scheduled while the page wasn't compositing frames, silently
freezing all future topic-list updates (not just renames) until a
full reload. Falls back to setTimeout when the document is hidden,
since that is never dropped the way the rAF was.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 18:07:41 +08:00
Austin 0331cb099c Merge branch 'dev' of https://git.118im.com/im_team/im_webapp into dev 2026-09-18 17:58:05 +08:00
Austin f4a6ff36b4 fix: realtime friend-request badge via connection-level info fallback
Server friend.request.* signals ride {info} frames addressed to the
recipient's own user id, but the SDK dispatcher resolves that topic via
its topic cache, where the me topic lives under the literal me -- the
lookup missed and the frame was dropped before any listener could see
it, so the 新朋友 badge and NewFriendsView list only updated after a
manual refresh or relogin. topics.ts now forwards frames whose topic is
not in the SDK cache (isTopicCached) through the connection-level
onInfoMessage hook to the same infoListeners used by topic-level
routing (no duplicate delivery), and AppShell invalidates the shared
['friend-requests'] query namespace on friend.request.* events, keeping
the pending badge live on any page. Zentao #3.
2026-09-18 17:42:48 +08:00
Your NameandClaude Sonnet 5 5748f1290d fix(groups): sync group rename in real time for owner and members
PATCH .../groups/{topic} pushes a custom {info what:"group",
event:"group.profile.updated"} that nothing recognized, so the
sidebar/header name (native SDK contact.public.fn) never refreshed
until an unrelated focus-triggered resync happened to touch it.
Add the missing branch to onInfo (mirrors the existing
group.dissolved/message.recalled handling) to re-pull public/desc
via topic.getMeta(...withDesc()), and trigger the same refresh from
the renamer's own save success since they won't receive their own
push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 17:36:11 +08:00
Your Name 0de8c0b5ac fix: message list shaking/flickering when scrolling to top or bottom
topic.onData = notify (wireTopicCallbacks) fires once per individual
message, not once per batch. Live-instrumented a real loadOlder() page
fetch (24 older messages): scrollHeight/scrollTop mutated in ~14
separate small steps over ~400ms, each one racing loadOlder()'s own
scroll-compensation and ConversationView's separate autoscroll-to-
bottom effect before the page had actually finished arriving -- that
rapid multi-step crawl is what reads as "shaking" rather than one
clean jump.

Coalesce notifications landing within the same animation frame into a
single flush, so a burst of onData calls (initial load, loadOlder(), a
fast run of incoming messages) triggers at most one re-render per
frame instead of N. messageSnapshots invalidation stays synchronous
(pure cache bookkeeping); only the listener callbacks that schedule
React's re-render are batched.

Live-verified with the same instrumentation: the same 24-message
loadOlder() fetch now produces 2 dom-mutation events instead of 14,
with scrollTop only moving once the batch has actually settled instead
of crawling in lockstep with a growing scrollHeight.
2026-09-18 17:11:40 +08:00
Your Name f5a512b8c9 fix: recall/edit not syncing to the other party in real time
docs/IM-前后端对接文档.md:416,1256 documents a custom {info what:"im",
event:"message.recalled"|"message.edited"} push to every other online
topic member, with an explicit instruction to refresh that message by
topic+seq+version rather than waiting for the user to re-enter the
conversation. Nothing recognized "im" as a valid `what` value (the
native SDK's own _routeInfo has no such case either, same gap as the
existing group.dissolved handling right above this fix), so the push
arrived over the wire but never triggered anything -- the peer's local
message cache kept the stale pre-recall/pre-edit content until some
unrelated full resync happened to refetch it.

Widen the `what` union (both the app-level RawTopicInfo and the SDK's
own onInfo callback type) to include 'im', and on that event issue the
same targeted single-message getMeta(...).withData(seq,seq+1,1) refetch
ConversationView's jump-to-message feature already established.

Live-verified: recalling a message now shows "⚠ 消息已删除" on the
other party's screen immediately, with no manual refresh.
2026-09-18 16:09:52 +08:00
Your Name 1a4a96461a feat: show group announcement read count and send read receipts
Backend added read_count/member_count/read_summary to the announcement
payload in today's deploy (previously confirmed absent). Wire it up:
- groups.ts: add the three fields to the GroupAnnouncement type.
- GroupAnnouncementModal.tsx: render "已读N/M" per README §4.8, and
  call readGroupAnnouncement() on open -- that endpoint existed in
  groups.ts already but was never called from anywhere, so opening an
  announcement never told the server this member had read it.
2026-09-18 16:05:04 +08:00
Your Name b4675b70e1 docs: correct root-cause comment on avatar CORS investigation
The OSS bucket's CORS rule does allow the app's auth headers -- just
scoped to the real web/web1-5.118im.com origins. The earlier failure
was from testing against a local dev origin, never in any CORS
whitelist, not from OSS lacking CORS entirely.
2026-09-18 15:18:32 +08:00
Your Name 74f4e753ef fix: don't route already-absolute avatar URLs through authenticated fetch
Default-avatar placeholders return avatar_file_id as a complete OSS URL
rather than a short file id. The previous fix stopped re-wrapping that
URL into a malformed /v0/file/s/https://... request, but still routed
it through useAuthedBlobUrl's authenticated fetch -- live-confirmed
that OSS bucket serves default avatars with no CORS headers at all, so
the cross-origin fetch carrying auth headers gets blocked at preflight
and the tile silently falls back to the plain letter. An absolute URL
needs no credentials; render it directly as a plain <img src> instead.
2026-09-18 15:13:27 +08:00
Your Name 7fff1a7b3a fix: call media-connect state, blocklist banner staleness, group rename, avatar URL
- stores/calls.ts, CallRoot.tsx: distinguish signaling-connected from
  actual media-joined (mediaJoined flag), so a call whose mic never
  attaches no longer shows a live timer/"网络良好"; propagate a
  disconnected reason on post-accept media failure so both sides agree
  on the outcome instead of one side seeing a normal "已结束".
- DetailColumn.tsx: invalidate conversation-context alongside blocks on
  block/unblock, so the composer's blocked-notice banner updates
  immediately instead of only after a window-focus refetch.
- GroupDetailColumn.tsx: add an owner-only group rename control
  (updateGroup was already implemented but never wired to any UI).
- Avatar.tsx: stop re-wrapping an already-absolute avatar_file_id (seen
  on default-avatar placeholders) into a malformed
  /v0/file/s/https://... request.
2026-09-18 15:01:08 +08:00
526 changed files with 3605 additions and 196106 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"comments": false
}
-4
View File
@@ -1,4 +0,0 @@
umd/* binary
package-lock.json binary
*.min.css binary
src/i18n.min/* binary
+56 -23
View File
@@ -1,39 +1,72 @@
name: Deploy im-webapp (k3s test cluster) name: Deploy im-webapp production
on: on:
push:
branches:
- pro
workflow_dispatch: workflow_dispatch:
inputs:
image_tag: concurrency:
description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)' group: production-im-webapp
required: false cancel-in-progress: false
permissions:
contents: read
env:
REGISTRY: git.olns.shop
IMAGE: git.olns.shop/pro/im_webapp
SERVICE: im-webapp
DEPLOY_HOST: 145.223.69.9
DEPLOY_USER: olns-deploy
jobs: jobs:
deploy: deploy:
runs-on: deploy-host runs-on: deploy-host
timeout-minutes: 60
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: 确定镜像tag - name: Log in to Gitea Container Registry
id: vars env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: | run: |
TAG="${{ github.event.inputs.image_tag }}" set -euo pipefail
[ -z "$TAG" ] && TAG=test printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u admin --password-stdin
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: 登录镜像仓库 - name: Build and push image
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin env:
TINODE_API_KEY: ${{ secrets.TINODE_API_KEY }}
- name: 构建并推送镜像 GITHUB_SHA: ${{ github.sha }}
run: | run: |
set -euo pipefail
SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
docker build \ docker build \
--build-arg TINODE_API_KEY="${{ secrets.TINODE_API_KEY }}" \ --build-arg TINODE_API_KEY="${TINODE_API_KEY}" \
-t git.118im.com/im_team/im-webapp:${{ steps.vars.outputs.tag }} . --build-arg TINODE_FILE_BASE="https://im-api.yuexuan.shop/v0/file" \
docker push git.118im.com/im_team/im-webapp:${{ steps.vars.outputs.tag }} -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" .
docker push "$IMAGE:pro"
docker push "$IMAGE:$SHORT_SHA"
- name: 部署到k3s测试集群 - name: Deploy Compose service
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: | run: |
mkdir -p ~/.kube set -euo pipefail
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config install -d -m 700 "$HOME/.ssh"
chmod 600 ~/.kube/config KEY_FILE="$(mktemp "$HOME/.ssh/olns-deploy.XXXXXX")"
kubectl rollout restart deployment/im-webapp -n im-webapp-system cleanup() {
kubectl rollout status deployment/im-webapp -n im-webapp-system --timeout=120s shred -u "$KEY_FILE" 2>/dev/null || true
}
trap cleanup EXIT
printf '%s\n' "$DEPLOY_SSH_KEY" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"
SSH_ARGS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=yes)
printf '%s' "$REGISTRY_TOKEN" | ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
"docker login git.olns.shop -u admin --password-stdin"
ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
"cd /opt/olns && docker compose pull im-webapp && docker compose up -d --no-deps --wait im-webapp"
-38
View File
@@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve TinodeWeb
title: ''
labels: bug
assignees: ''
---
If you want to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. Mac OS, Windows 10 Home]
- Browser [e.g. Chrome, Safari]
- TinodeWeb Version [e.g. 0.16.6]
**Console log**
Please attach or insert console log which illustrates the problem. Please use the log produced by `index-dev.html`, not `index.html`; `index.html` uses minified javascript which produces unusable logs.
**Additional context**
Add any other context about the problem here.
-23
View File
@@ -1,23 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
If you want to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+3 -2
View File
@@ -1,7 +1,8 @@
.DS_Store .DS_Store
.git.worktree-pointer.bak
node_modules node_modules
src/i18n/srci/ .idea/
webapp.code-workspace.code-workspace *.code-workspace
# Local developer overrides — never commit real keys. # Local developer overrides — never commit real keys.
.env.local .env.local
+9 -9
View File
@@ -1,9 +1,6 @@
# ---- Stage 1: build static assets ---- # ---- Stage 1: build static assets ----
# The frontend was fully rewritten from scratch under web/ (Vite + React + TS) on branch # The active frontend lives under web/ (Vite + React + TypeScript). The legacy
# redesign/desktop-multicolumn, merged into dev 2026-09-09 -- this replaces the old webpack build # root-level Webpack application has been removed; production must only build web/.
# that used to live at the repo root (umd/, css/base.min.css, index.html, etc.). See web/README-
# equivalent context in the redesign plan for why: this Dockerfile previously never touched web/ at
# all, so every "deploy" kept shipping the old app regardless of what was on dev.
FROM node:22-alpine AS build FROM node:22-alpine AS build
WORKDIR /app/web WORKDIR /app/web
@@ -17,6 +14,11 @@ COPY web/. .
ARG TINODE_API_KEY= ARG TINODE_API_KEY=
ENV VITE_TINODE_API_KEY=$TINODE_API_KEY ENV VITE_TINODE_API_KEY=$TINODE_API_KEY
# File downloads must use the canonical IM API origin. The web origin has its own independently
# deployed path router and may otherwise send /v0/file requests to an older IM server.
ARG TINODE_FILE_BASE=https://api.118im.com/v0/file
ENV VITE_TINODE_FILE_BASE=$TINODE_FILE_BASE
RUN npm run build RUN npm run build
# ---- Stage 2: serve with nginx ---- # ---- Stage 2: serve with nginx ----
@@ -26,10 +28,8 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
WORKDIR /usr/share/nginx/html WORKDIR /usr/share/nginx/html
# Vite's build output is fully self-contained: index.html + favicon.svg/icons.svg (from web/public/, # Vite's build output is fully self-contained: index.html, public assets and
# copied verbatim) + assets/ (content-hashed JS/CSS chunks) -- no separate umd/css/img/audio/ # content-hashed JS/CSS chunks.
# manifest/service-worker/firebase files the way the old build had, none of that machinery exists
# in the new app.
COPY --from=build /app/web/dist/. ./ COPY --from=build /app/web/dist/. ./
HEALTHCHECK --interval=1m --timeout=3s --start-period=10s \ HEALTHCHECK --interval=1m --timeout=3s --start-period=10s \
+31 -76
View File
@@ -1,89 +1,44 @@
# Tinode Web # 迅工捷使 Web
## Briefing 当前前端位于 `web/`,技术栈为 React、TypeScript 和 Vite。仓库根目录不再保留旧版 Tinode Webpack 应用及其构建产物。
Single-page web chat application for [Tinode](https://github.com/tinode/chat/). The app is built on ## 本地开发
[React](https://reactjs.org/). The [Tinode javascript SDK](https://github.com/tinode/tinode-js/) has no external dependencies.
Overall, it's a lot like open source WhatsApp or Telegram web apps.
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist, some features are missing. The app was tested in the latest Chrome & Firefox only. NPM package is [available](https://www.npmjs.com/package/tinode-webapp). ```bash
cd web
npm ci
npm run dev
```
Try possibly newer or older version live at https://sandbox.tinode.co/. ## 检查与构建
<a href="https://web.tinode.co/"><img src="web-app.svg" height=36></a> ```bash
cd web
node --test tests/*.test.mjs tests/*.test.ts
npm run lint
npm run build
```
For demo access and other instructions [see here](https://github.com/tinode/chat/#demosandbox). 生产构建输出到 `web/dist/`。
## Installing and running ## Docker 部署
This is _NOT_ a standalone app, this is just a frontend, a client. It requires a [backend](https://github.com/tinode/chat/). See [installation instructions](https://github.com/tinode/chat/blob/master/INSTALL.md). See instructions for [enabling push notifications](push.md). 根目录 `Dockerfile` 只复制并构建 `web/`:
## Getting support ```bash
docker build \
--build-arg TINODE_API_KEY="<部署密钥>" \
--build-arg TINODE_FILE_BASE="https://api.118im.com/v0/file" \
-t im-webapp .
```
* Read [client-side](http://tinode.github.io/js-api/) and [server-side](https://github.com/tinode/chat/blob/master/docs/API.md) API documentation. 生产环境通过 `TINODE_FILE_BASE` 固定附件下载入口;本地开发不设置该变量时,继续使用租户解析结果和 Vite 代理。
* For support, general questions, discussions post to [https://groups.google.com/d/forum/tinode](https://groups.google.com/d/forum/tinode).
* For bugs and feature requests [open an issue](https://github.com/tinode/webchat/issues/new).
* Use https://tinode.co/contact for commercial inquiries.
## Helping out 测试集群通过 Gitea Actions 的 `Deploy im-webapp (k3s test cluster)` 工作流手动发布。
* If you appreciate our work, please help spread the word! Sharing on Reddit, HN, and other communities helps more than you think. ## 目录
* Consider buying paid support: https://tinode.co/support.html
* If you are a software developer, send us your pull requests with bug fixes and new features.
* If you use the app and discover bugs or missing features, let us know by filing bug reports and feature requests. Vote for existing [feature requests](https://github.com/tinode/chat/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A%22feature+request%22) you find most valuable.
* If you speak a language other than English, [translate](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp) the apps into your language. You may also review and improve existing translations.
* If you are a UI/UX expert, help us polish the app UI.
* Use it: install it for your colleagues or friends at work or at home.
## Internationalization - `web/`:当前 Vite 前端源码、测试和静态资源
- `docker/`:Nginx 配置
The app is fully internationalized using [React-Intl](https://github.com/formatjs/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `#?hl=XX` parameter to the URL when starting the app, e.g. https://web.tinode.co/#?hl=fr. - `docs/`:设计及实施文档
- `.gitea/workflows/`:测试集群部署工作流
The app is currently available in the following languages:
* English - default
* Arabic
* Chinese simplified
* Chinese traditional
* French
* German
* Italian
* Korean
* Romanian
* Russian
* Spanish
* Thai
* Ukrainian
* Vietnamese
More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp).
## Not done yet
* End-to-End encryption.
* Emoji support is weak.
* Mentions, hashtags.
* Previews not generated for links and docs.
* Location and contact sharing.
* Recording video messages.
## Other
* Demo avatars and some other graphics are from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
* Background patterns from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
* [Fonts](https://fonts.google.com/) and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web) from Google under [license](https://developers.google.com/terms) which permits commercial and non-commercial use.
* Push notifications built with https://firebase.google.com/.
## Responsive design
### Desktop screenshot
<p align="center">
<img src="web-desktop.jpg" alt="Desktop web: full app" width=810 />
</p>
### Mobile screenshots
<p align="center">
<kbd><img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /></kbd> <kbd><img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /></kbd> <kbd><img src="web-mob-chat-dark.png" alt="Mobile web: chat dark mode" width=250 /></kbd> <kbd><img src="web-mob-info.png" alt="Mobile web: topic info" width=250 /></kbd> <kbd><img src="web-mob-forward.png" alt="Mobile web: forward message" width=250 /></kbd> <kbd><img src="web-mob-video-call.png" alt="Mobile web: video call" width=250 /></kbd>
</p>
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-15171
View File
File diff suppressed because it is too large Load Diff
-1
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
# Web Clipboard Image Compatibility Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Broaden the new Web composer's image-paste formats without changing immediate send or native text paste.
**Architecture:** Extract clipboard inspection into a small dependency-free module. The React paste callback synchronously snapshots event data and calls `preventDefault()` only for an image or an image-looking fallback; asynchronous data-URL/Clipboard API decoding then feeds the existing `sendImage` path.
**Tech Stack:** React 19, TypeScript, Vite, Node 25 built-in `node:test`, Playwright CLI.
---
## File map and boundaries
- Create `web/src/features/messages/clipboard-image.ts`: clipboard format selection, HTML data-image extraction, async Blob conversion, Clipboard API fallback. It must not import React, Tinode, or UI state.
- Create `web/tests/clipboard-image.test.ts`: direct behavior tests for the helper; no new test dependency.
- Modify `web/src/features/messages/ConversationView.tsx:939-962`: replace the narrow paste loop and reuse existing `sendImage`/error toast. Do not change `handlePickImage`, `sendImage`, drafts, or other views.
- Modify `web/package.json`: add `test:clipboard` invoking `node --test tests/clipboard-image.test.ts`.
## Task 1: Clipboard extraction with test-first coverage
**Files:** Create `web/tests/clipboard-image.test.ts`; create `web/src/features/messages/clipboard-image.ts`; modify `web/package.json`.
- [ ] **Step 1: Add a failing assertion and API stubs.** In the test, import `firstImageFile`, `htmlDataImageUrl`, `shouldReadClipboard`, `fileFromDataImageUrl`, and `readClipboardImage` from `../src/features/messages/clipboard-image.ts`. Use `node:test`/`node:assert/strict`. Start with `new File(['png'], 'shot.png', {type:'image/png'})` in `items[0].getAsFile()` and assert `firstImageFile(data) === file`. Export typed stub functions returning `null`/`false` in the new helper solely so the test runs to an assertion failure.
```ts
import assert from 'node:assert/strict';
import test from 'node:test';
import { firstImageFile } from '../src/features/messages/clipboard-image.ts';
test('reads an image file from clipboard items', () => {
const file = new File(['png'], 'shot.png', { type: 'image/png' });
const data = { items: [{ kind: 'file', type: 'image/png', getAsFile: () => file }], files: [] } as unknown as DataTransfer;
assert.equal(firstImageFile(data), file);
});
```
- [ ] **Step 2: Run `cd web && node --test tests/clipboard-image.test.ts`; confirm the first assertion fails (`actual: null`, expected File), not a loader/type error.**
- [ ] **Step 3: Implement the smallest `firstImageFile(data: DataTransfer | null): File | null` that checks each `kind=file` item via `getAsFile()`, accepts `file.type` or an image `item.type`, then checks `data.files` even when `items` exists. If only `item.type` supplies MIME, wrap the File with that MIME. Return the first image only.**
- [ ] **Step 4: Rerun the focused test; confirm pass. Add one failing test at a time for JPEG/BMP, empty `item.type` with valid File MIME, null `getAsFile()` followed by `files`, a non-image file, and two images selecting the first. Run red then green for each.**
- [ ] **Step 5: Add failing HTML tests:** an `<img src="data:image/png;base64,...">` URL is returned; a remote `https:` image, plain HTML text, and a `data-src` attribute are not. Implement `htmlDataImageUrl(data)` by reading `getData('text/html')` synchronously and matching only an actual `src` attribute on an `<img>`, with `data:image/` scheme. Add a failing test that `fileFromDataImageUrl()` converts a valid data URL to an image File and rejects malformed data. Implement it with `fetch(dataUrl)`, `Blob`, and `new File(...)`; never fetch a non-data URL.
- [ ] **Step 6: Add failing fallback-decision tests:** `shouldReadClipboard(data)` is false for plain text and ordinary HTML, true for a file/image hint without usable file or a completely empty event. Implement with `types`, `items`, and `getData('text/plain')`, without asynchronous event reads.
- [ ] **Step 7: Add failing Clipboard API tests:** injected `read()` returning a `ClipboardItem` with `image/png` produces a File; a text-only item returns null; permission rejection propagates. Implement `readClipboardImage(clipboard?: Pick<Clipboard,'read'>): Promise<File | null>` by trying image MIME types, `getType()`, and wrapping the Blob. No navigator access inside this helper, so tests can inject a stub.
```ts
const clipboard = { read: async () => [{ types: ['image/png'], getType: async () => new Blob(['png'], { type: 'image/png' }) }] } as unknown as Pick<Clipboard, 'read'>;
const image = await readClipboardImage(clipboard);
assert.equal(image?.type, 'image/png');
```
- [ ] **Step 8: Add `"test:clipboard": "node --test tests/clipboard-image.test.ts"` to `web/package.json`. Run `npm run test:clipboard`; expected all tests pass. Commit only these files with `git add web/src/features/messages/clipboard-image.ts web/tests/clipboard-image.test.ts web/package.json && git commit -m 'test: cover clipboard image formats'`.**
## Task 2: Integrate with the composer
**Files:** Modify `web/src/features/messages/ConversationView.tsx:939-962`; test `web/tests/clipboard-image.test.ts`.
- [ ] **Step 1: Add a failing orchestration test:** introduce `handleImagePaste(data, preventDefault, onImage, onError, clipboard)` as a named export and test that PNG calls `preventDefault` synchronously and then `onImage`, plain text does neither, HTML data-image claims before awaiting conversion, and Clipboard API failure calls `onError`. Run this test red (first as a typed stub returning `Promise.resolve(false)`), then implement the orchestrator in `clipboard-image.ts`. It must call `preventDefault` before its first `await`, and must not catch an exception thrown by `onImage` as a clipboard-read failure.
```ts
let prevented = false;
let sent: File | null = null;
const pending = handleImagePaste(data, () => { prevented = true; }, file => { sent = file; }, () => { throw new Error('unexpected'); });
assert.equal(prevented, true);
await pending;
assert.equal(sent, file);
```
- [ ] **Step 2: Replace the existing `handleComposerPaste` loop with a thin call to `handleImagePaste`.** Pass `e.clipboardData`, `() => e.preventDefault()`, a callback that calls `consumePendingReply()` then the existing `sendImage(topic,file,reply).catch(...)`, a callback that calls `showError(undefined, '无法读取剪贴板图片')`, and `navigator.clipboard`. This preserves existing send behavior and does not retain the React event across an asynchronous boundary. Do not broaden to previews or multi-image sends.
```tsx
const handleComposerPaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
void handleImagePaste(
e.clipboardData,
() => e.preventDefault(),
(file) => {
const reply = consumePendingReply();
sendImage(topic, file, reply).catch((err) => showError(err, '图片发送失败'));
},
() => showError(undefined, '无法读取剪贴板图片'),
navigator.clipboard,
);
};
```
- [ ] **Step 3: Run `cd web && npm run test:clipboard && npm run build && npm run lint`; all commands must exit 0. Review `git diff --check` and `git diff -- web/src/features/messages/ConversationView.tsx` for scope and native-text behavior. Commit only the touched files with `git add web/src/features/messages/ConversationView.tsx web/src/features/messages/clipboard-image.ts web/tests/clipboard-image.test.ts && git commit -m 'fix: support more clipboard image formats'`.**
## Task 3: Browser verification and handoff
**Files:** No production changes unless a targeted test finds a defect.
- [ ] **Step 1: Start local Vite (`cd web && npm run dev -- --host 127.0.0.1`) and use Playwright CLI on the local origin. Import `/src/features/messages/clipboard-image.ts` in the browser, paste a real PNG ClipboardItem, and verify it appears as `kind=file`, `type=image/png`, and is selected. Separately dispatch a text-only ClipboardEvent and confirm `shouldReadClipboard` is false and native default is not prevented. Test HTML data image selection and remote HTML exclusion.**
- [ ] **Step 2: Rerun `npm run test:clipboard`, `npm run build`, `npm run lint`, and `git diff --check` fresh after any fixes. Check `git status --short` and report exact commits/files. Do not push or deploy. Explain that macOS Chromium/browser-event simulation is not a real Windows WeChat end-to-end test.**
## Self-review against the approved spec
Items/files, MIME fallback, HTML data image, Clipboard API, text passthrough, error feedback, first-image-only behavior, no remote fetch, test-first, browser verification, and no deployment are all assigned above. The only unavoidable limit is native Windows clipboard conversion before JavaScript receives the event; the final report must not claim that was verified.
@@ -0,0 +1,151 @@
# Web Brand And Message Sound Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将生产 Web 品牌统一为“迅工捷使”,精简登录页,并为符合提醒条件的新消息播放用户提供的提示音。
**Architecture:** 继续以 `web/src/lib/config.ts` 作为显示品牌名来源,认证页和系统消息只消费该常量;品牌图标和 MP3 作为 Vite `public/` 静态资源。消息声音接入现有 `push.ts` 新消息扫描链路,在浏览器通知权限判断之前复用“本人消息、当前可见会话、会话免打扰”的过滤结果。
**Tech Stack:** React 19、TypeScript、Vite、Node test runner、Jiti。
---
### Task 1: 写品牌和提示音红测
**Files:**
- Create: `web/tests/brand-and-notification-sound.test.mjs`
- [ ] **Step 1: 写失败测试**
创建 Node 测试,核心断言如下;可按实际导出补齐 fake `Audio` 的类型字段,但不得降低断言:
```js
const webRoot = new URL('../', import.meta.url);
assert.match(await readFile(new URL('index.html', webRoot), 'utf8'), /<title>迅工捷使<\/title>/);
assert.doesNotMatch(await readFile(new URL('src/features/auth/LoginPage.tsx', webRoot), 'utf8'), /桌面端|下载桌面客户端|iOS \/ Android|帮助中心/);
assert.doesNotMatch(await readFile(new URL('src/features/auth/TenantEntryPage.tsx', webRoot), 'utf8'), /MeetingIM/);
assert.doesNotMatch(await readFile(new URL('src/features/system/SystemMessagesView.tsx', webRoot), 'utf8'), /MeetingIM 安全团队/);
assert.equal(audioSource, '/message-notification.mp3');
assert.equal(playCount, 1);
```
- [ ] **Step 2: 运行测试确认失败**
Run: `node --experimental-strip-types --test tests/brand-and-notification-sound.test.mjs`
Expected: FAIL,因为品牌仍是 `MeetingIM`,提示音模块和静态资源尚不存在。
### Task 2: 更新品牌和登录页
**Files:**
- Modify: `web/index.html`
- Modify: `web/src/lib/config.ts`
- Modify: `web/src/features/auth/LoginPage.tsx`
- Modify: `web/src/features/auth/TenantEntryPage.tsx`
- Modify: `web/src/features/system/SystemMessagesView.tsx`
- Create: `web/public/app-icon.png`
- Delete: `web/public/favicon.svg`
- Modify: `web/src/lib/notifications/tabIndicator.ts`
- Modify: `web/src/lib/notifications/push.ts`
- [ ] **Step 1: 复制官方品牌图标**
将 `../im-ios-website/dist/assets/app-icon.png` 原样复制为 `web/public/app-icon.png`,所有 Web 图标引用统一指向它。
- [ ] **Step 2: 最小化更新可见品牌**
把浏览器标题、认证页和系统消息署名改为“迅工捷使”;`APP_NAME` 保留 `MeetingIM/<version>` 作为现有 Tinode 客户端技术标识,避免把显示品牌替换扩大成协议改名。
```ts
export const BASE_APP_NAME = '迅工捷使';
export const APP_NAME = `MeetingIM/${__APP_VERSION__}`;
```
认证页统一使用:
```tsx
<img src="/app-icon.png" alt="" className="h-full w-full rounded-[inherit]" />
<strong>{BASE_APP_NAME}</strong>
```
- [ ] **Step 3: 精简登录页**
说明文案改为“单聊、群聊、通讯录、文件与收藏、音视频通话,消息与回执在手机端保持一致。”,删除底部三个入口。
```tsx
<p className="mt-4 max-w-md text-base text-white/80">
单聊、群聊、通讯录、文件与收藏、音视频通话,消息与回执在手机端保持一致。
</p>
```
### Task 3: 接入消息提示音
**Files:**
- Create: `web/src/lib/notifications/sound.ts`
- Modify: `web/src/lib/notifications/push.ts`
- Create: `web/public/message-notification.mp3`
- [ ] **Step 1: 复制用户提供的提示音**
将 `/Users/aa/Downloads/universfield-new-notification-012-363675.mp3` 原样复制为 `web/public/message-notification.mp3`。
- [ ] **Step 2: 实现安全播放器**
首次需要时创建 `Audio('/message-notification.mp3')`,每次播放前设置 `currentTime = 0`;对同步异常和 `play()` Promise 拒绝均静默处理。
```ts
let messageAudio: HTMLAudioElement | null = null;
export function playMessageSound(): void {
try {
messageAudio ||= new Audio('/message-notification.mp3');
messageAudio.currentTime = 0;
void messageAudio.play().catch(() => {});
} catch {
// Optional audio must never interrupt message delivery.
}
}
```
- [ ] **Step 3: 接入新消息过滤链路**
把“本人消息、当前可见会话、会话免打扰”抽成通知与声音共享的判断。通过判断后先播放声音;仅当 Notification API 可用且权限为 `granted` 时再显示系统通知。
```ts
if (!shouldAlert(topicName, latest)) return;
playMessageSound();
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
fireNotification(topicName, latest);
}
```
- [ ] **Step 4: 跑绿测**
Run: `node --experimental-strip-types --test tests/*.test.ts tests/*.test.mjs`
Expected: 现有 6 项和新增测试全部 PASS。
### Task 4: 验证、提交与部署
**Files:**
- Verify only
- [ ] **Step 1: 静态验证**
Run: `npm run build && npm run lint`
Expected: 构建退出码 0;lint 无 error,允许记录既有 warning。
- [ ] **Step 2: 完整性验证**
Run: `git diff --check`
Expected: 无输出,退出码 0;MP3 和应用图标的 SHA-256 与来源文件一致。
- [ ] **Step 3: 提交并推送**
提交精确修改文件到 `dev`,推送前先 fetch 并确认无远端分叉。
- [ ] **Step 4: 部署和公网回归**
触发 Gitea `deploy.yml`,等待成功,确认 K3s Deployment 2/2;访问 `https://web.118im.com` 验证标题、品牌、登录页删减及双账号新消息提醒路径。
@@ -0,0 +1,23 @@
# Web 聊天图片粘贴兼容设计
日期:2026-09-29。范围仅限 `web/` 新版聊天输入框的图片粘贴;不改旧版、后端、图片选择器或部署配置。
## 现状与目标
当前输入框只读取 `clipboardData.items` 中 `kind=file` 且 `item.type` 为 `image/*` 的首个项目。Windows 微信截图在不同浏览器中的剪贴板暴露形式尚无现场记录,因此不能断言根因;但 `files`、文件本身 MIME、内嵌 HTML 图片和浏览器 Clipboard API 均是当前实现未覆盖的可读路径。线上已经部署了现有粘贴功能。此前“多图暂存并配文字发送”按用户反馈撤回,本次保持**识别到首张图片后立即发送**,不加入预览、多图或图文合并。
## 方案
把图片提取逻辑放入一个可独立测试的小模块,输入为粘贴事件中的 `DataTransfer`,输出为首个可发送的 `File` 或“未取得图片”。按顺序尝试:
1. 同步检查 `items`:对 `kind=file` 调用 `getAsFile()`,以所得文件的 MIME 为准,`item.type` 仅作补充;然后检查 `files`,避免浏览器只填充其中一个集合。
2. 同步检查 `text/html` 中的 `<img src="data:image/...">`;只解码本地内嵌图片,不请求远程 URL,不执行 HTML。纯文字和普通 HTML 粘贴保持浏览器原生行为。
3. 对没有可用同步图片、也没有纯文字,且事件带图片/文件线索或完全没有数据类型的情况,尝试 `navigator.clipboard.read()` 读取浏览器提供的 `image/*` Blob。仅含普通 HTML 的事件不走此兜底。此路径可能受权限或浏览器支持限制;网页无法越过浏览器直接读取 Windows 原生 CF_DIB 等格式。
只有确定要接管图片粘贴或进入无文字的异步兜底时,才在事件回调内同步 `preventDefault()`。不得在 `await` 之后访问原粘贴事件的数据。提取成功后仍调用现有 `sendImage`;提取或读取失败时对明确的图片粘贴给出简短提示,发送失败继续沿用现有错误提示。不得让纯文字粘贴触发发送或权限请求。
## 验收与测试
先写失败测试,再改实现。覆盖:`items` 的 PNG/JPEG/BMP、`item.type` 缺失但文件 MIME 正确、仅 `files`、`getAsFile()` 为空时回退、HTML 内嵌图片、Clipboard API 返回图片、权限拒绝、纯文字原生粘贴、远程 HTML 图片不抓取、多个图片只取首张。运行定向测试、前端构建和 lint;再用真实浏览器派发图片及文字粘贴事件,核对是否接管和既有即时发送路径。若无 Windows/微信运行环境,最终明确说明这一端到端组合尚未实测,不宣称已覆盖所有微信版本。
本次只做本地代码与验证;不推送、不部署。
@@ -0,0 +1,34 @@
# Web 品牌与消息提示音设计
## 目标
将当前生产 Web 的用户可见品牌统一为“迅工捷使”,精简登录页指定文案,并使用用户提供的 MP3 为新收到的消息播放提示音。
## 范围
- 当前 Dockerfile 实际构建的 `web/` 目录。
- 浏览器标题、企业码页、登录页、系统消息署名和应用图标。
- 登录页说明改为“消息与回执在手机端保持一致”,删除底部三个入口。
- 复用 Android/iOS 已有蓝底扳手图标。
- 新消息提示音使用用户提供的 `universfield-new-notification-012-363675.mp3`。
## 行为
- 只为他人发来的新消息播放一次提示音。
- 正在可见地查看对应会话时不播放。
- 会话设置为消息免打扰时不播放。
- 声音播放失败不得影响消息接收、列表更新或浏览器通知。
- 浏览器系统通知权限不控制声音;声音与现有通知共用同一条新消息判定链路。
## 明确不改
- 后端代码和后端默认推送、短信名称。
- `X-MeetingIM-APIKey`、`X-MeetingIM-Auth` 等协议头。
- `meetingim-session` 等已有存储键。
- 仓库名、镜像名、域名和旧版未部署 Web 入口。
## 验证
- 自动测试覆盖品牌文字、登录页删减、品牌图片引用和提示音播放器。
- Web 全量测试、构建、lint 和 `git diff --check`。
- 部署后实测企业码页、登录页、浏览器标题、图标,以及双账号新消息提示音路径。
-15
View File
@@ -1,15 +0,0 @@
// Get these values for your project from the https://console.firebase.google.com/
const FIREBASE_INIT = {
// Set up a web application in console then get the config object.
// The following keys are copied from the object:
apiKey: "AIzaSyD6X4ULR-RUsobvs1zZ2bHdJuPz39q2tbQ",
messagingSenderId: "114126160546",
projectId: "tinode-1000",
appId: "1:114126160546:web:aca6ea2981feb81fb44dfb",
// Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates.
// This value IS NOT included in the generated js config you get from
// the firebase console. It needs to be added separately.
messagingVapidKey: "BOgQVPOMzIMXUpsYGpbVkZoEBc0ifKY_f2kSU5DNDGYI6i6CoKqqxDd7w7PJ3FaGRBgVGJffldETumOx831jl58",
// Google analytics tag. Uncomment and use one you got from https://analytics.google.com/.
// measurementId: "G-ABCDEF1234"
};
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

-18
View File
@@ -1,18 +0,0 @@
{
"patt": [
{"name": "a00.png", "size": 200}, {"name": "a01.jpg", "size": 384}, {"name": "a02.jpg", "size": 384},
{"name": "a03.jpg", "size": 384}, {"name": "a04.png", "size": 400}, {"name": "a05.jpg", "size": 400},
{"name": "a06.jpg", "size": 256}, {"name": "a07.jpg", "size": 384}, {"name": "a08.jpg", "size": 384},
{"name": "a09.jpg", "size": 400}, {"name": "a10.jpg", "size": 256}, {"name": "a11.png", "size": 200}
],
"img": [
{"name": "w01.jpg", "pr": "p01.jpg"}, {"name": "w02.jpg", "pr": "p02.jpg"}, {"name": "w03.jpg", "pr": "p03.jpg"},
{"name": "w04.jpg", "pr": "p04.jpg"}, {"name": "w05.jpg", "pr": "p05.jpg"}, {"name": "w06.jpg", "pr": "p06.jpg"},
{"name": "w07.jpg", "pr": "p07.jpg"}, {"name": "w08.jpg", "pr": "p08.jpg"}, {"name": "w09.jpg", "pr": "p09.jpg"},
{"name": "w10.jpg", "pr": "p10.jpg"}, {"name": "w11.jpg", "pr": "p11.jpg"}, {"name": "w12.jpg", "pr": "p12.jpg"},
{"name": "w13.jpg", "pr": "p13.jpg"}, {"name": "w14.jpg", "pr": "p14.jpg"}, {"name": "w15.jpg", "pr": "p15.jpg"},
{"name": "w16.jpg", "pr": "p16.jpg"}, {"name": "w17.jpg", "pr": "p17.jpg"}, {"name": "w18.jpg", "pr": "p18.jpg"},
{"name": "w19.jpg", "pr": "p19.jpg"}, {"name": "w20.jpg", "pr": "p20.jpg"}, {"name": "w21.jpg", "pr": "p21.jpg"},
{"name": "w22.jpg", "pr": "p22.jpg"}, {"name": "w23.jpg", "pr": "p23.jpg"}, {"name": "w24.jpg", "pr": "p24.jpg"}
]
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
<rect width="192" height="192" rx="26" fill="#ffffff"/>
<path transform="rotate(-18 96 96)" fill="#1677ff"
d="M96,13.5 A82.5,82.5 0 0 1 178.5,96 L178.5,96 A82.5,82.5 0 0 1 96,178.5 L43.2,178.5 A29.7,29.7 0 0 1 13.5,148.8 L13.5,96 A82.5,82.5 0 0 1 96,13.5 Z"/>
<text x="96" y="98" text-anchor="middle" dominant-baseline="central"
font-family="Roboto, Arial, sans-serif" font-weight="700" font-size="77" fill="#ffffff">M</text>
<circle cx="156" cy="156" r="30" fill="#ffffff"/>
<circle cx="156" cy="156" r="24" fill="#3ba55c"/>
</svg>

Before

Width:  |  Height:  |  Size: 614 B

-7
View File
@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
<rect width="192" height="192" rx="26" fill="#ffffff"/>
<path transform="rotate(-18 96 96)" fill="#1677ff"
d="M96,13.5 A82.5,82.5 0 0 1 178.5,96 L178.5,96 A82.5,82.5 0 0 1 96,178.5 L43.2,178.5 A29.7,29.7 0 0 1 13.5,148.8 L13.5,96 A82.5,82.5 0 0 1 96,13.5 Z"/>
<text x="96" y="98" text-anchor="middle" dominant-baseline="central"
font-family="Roboto, Arial, sans-serif" font-weight="700" font-size="77" fill="#ffffff">M</text>
</svg>

Before

Width:  |  Height:  |  Size: 510 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

-20
View File
@@ -1,20 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 500">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1a2c52"/>
<stop offset="1" stop-color="#0e1730"/>
</linearGradient>
</defs>
<rect width="750" height="500" fill="url(#bg)"/>
<g transform="translate(375 168)">
<rect x="-70" y="-70" width="140" height="140" rx="19" fill="#ffffff"/>
<path transform="rotate(-18)" fill="#1677ff"
d="M0,-60 A60,60 0 0 1 60,0 L60,0 A60,60 0 0 1 0,60 L-38.4,60 A21.6,21.6 0 0 1 -60,38.4 L-60,0 A60,60 0 0 1 0,-60 Z"/>
<text x="0" y="2" text-anchor="middle" dominant-baseline="central"
font-family="Roboto, Arial, sans-serif" font-weight="700" font-size="56" fill="#ffffff">M</text>
</g>
<text x="375" y="345" text-anchor="middle"
font-family="Roboto, Arial, sans-serif" font-weight="700" font-size="56" fill="#ffffff">MeetingIM</text>
<text x="375" y="392" text-anchor="middle"
font-family="Roboto, Arial, sans-serif" font-weight="400" font-size="24" fill="#aab4c8">即时通讯</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg height="24" viewBox="0 -960 960 960" width="24" fill="#5f6368" xmlns="http://www.w3.org/2000/svg"><path d="M197-706l-84-85 56-57 85 85zM791-112l-85-85 57-57 84 85zM0-440v-80h120v80zM840-440v-80h120v80zM169-113l-57-56 85-85 57 57zM763-706l-57-57 85-84 57 56zM440 0v-120h80V0zM440-840v-120h80v120zM743-380a766.09 766.09 0 01-39.5-11c-13-4-25.833-8.667-38.5-14 5.333-12 9.167-24.167 11.5-36.5 2.333-12.333 3.5-25.167 3.5-38.5 0-55.333-19.5-102.5-58.5-141.5S535.333-680 480-680c-13.333 0-26.167 1.167-38.5 3.5-12.333 2.333-24.5 6.167-36.5 11.5-5.333-12.667-9.833-25.333-13.5-38a1098.034 1098.034 0 01-10.5-39c16-6 32.333-10.5 49-13.5s33.667-4.5 51-4.5c78 0 144.167 27.167 198.5 81.5S761-558 761-480c0 17.333-1.5 34.333-4.5 51s-7.5 33-13.5 49z" fill="orange"/><path d="M453-452c34 34 72 63.167 114 87.5s87 43.167 135 56.5c-26.667 34-59.333 60.5-98 79.5S524.333-200 481-200c-78 0-144.167-27.167-198.5-81.5S201-402 201-480c0-43.333 9.5-84.333 28.5-123s45.5-71.333 79.5-98c13.333 48 32.167 93 56.5 135S419-486 453-452z" fill="#0c80d6"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-61
View File
@@ -1,61 +0,0 @@
<!-- DEVELOPMENT version of the web app. -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>MeetingIM</title>
<meta charset="utf-8" />
<!-- Real (History API) routing means the address bar can show a deep, multi-segment path like
/messages-home/grpXXX -- every asset reference below is a relative path (css/base.css,
umd/index.dev.js, etc.), which would otherwise resolve against that deep path instead of the
app's actual root on a deep-linked load or a refresh, 404ing everything. This pins relative
resolution to root regardless of the current path. -->
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="MeetingIM, version for developers" />
<meta name="description" content="MeetingIM instant messenger, development version" />
<meta name="application-url" content="https://web.118im.com/">
<!-- Browser styling for Chrome, FF, Opera -->
<meta name="theme-color" content="#3949AB" />
<!-- Ask Google not to offer page translation -->
<meta name="google" value="notranslate" />
<!-- Open Graph description -->
<meta name="og:url" content="https://web.118im.com/">
<meta name="og:title" content="MeetingIM Development">
<meta name="og:image" content="img/og-logo.jpeg">
<meta name="og:description" content="MeetingIM instant messenger, development version" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
<link rel="canonical" href="https://web.118im.com/" />
<!-- Google's Roboto font -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<!-- Google's material design icons -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- Polyfill Intl.Segmenter for Firefox 124 and earlier. This can be safely removed in the end of 2024. -->
<script crossorigin="anonymous" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></script>
<!-- ReactJS -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js"></script>
<!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@7/react-intl.iife.js"></script>
<!-- Displaying QR code with ID of the topic -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
<div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser.
MeetingIM does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.
</div>
</div></noscript>
<div id="mountPoint"></div>
<script src="firebase-init.js"></script>
<script src="umd/tinode.dev.js"></script>
<script src="umd/index.dev.js"></script>
</body>
</html>
-48
View File
@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>MeetingIM</title>
<meta charset="utf-8" />
<!-- Real (History API) routing means the address bar can show a deep, multi-segment path like
/messages-home/grpXXX -- every asset reference below is a relative path (css/base.min.css,
umd/index.prod.js, etc.), which would otherwise resolve against that deep path instead of the
app's actual root on a deep-linked load or a refresh, 404ing everything. This pins relative
resolution to root regardless of the current path. -->
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="application-name" content="MeetingIM" />
<meta name="description" content="MeetingIM instant messenger in a browser" />
<meta name="application-url" content="https://web.118im.com/">
<meta name="theme-color" content="#3949AB" />
<meta name="google" value="notranslate" />
<meta name="format-detection" content="telephone=no" />
<meta name="og:url" content="https://web.118im.com/">
<meta name="og:title" content="MeetingIM">
<meta name="og:image" content="img/og-logo.jpeg">
<meta name="og:description" content="MeetingIM instant messenger in a browser" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
<link rel="canonical" href="https://web.118im.com/" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@7.1.6/react-intl.iife.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
<div id="noscript-content">It appears JavaScript is turned off or is not supported by your browser.
MeetingIM does not work without JavaScript. Please enable JavaScript by changing your browser options,
then <a href="">try again</a>.</div></div></noscript>
<div id="mountPoint"></div>
<script src="firebase-init.js"></script>
<script src="umd/tinode.prod.js"></script>
<script src="umd/index.prod.js"></script>
</body>
</html>
-34
View File
@@ -1,34 +0,0 @@
{
"name": "MeetingIM",
"short_name": "MeetingIM",
"description": "MeetingIM instant messenger.",
"categories": ["chat", "communication", "productivity"],
"icons": [
{
"src":"img/logo96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src":"img/logo192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "img/logo.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"start_url": "/",
"background_color": "silver",
"theme_color": "#3949AB",
"display": "standalone",
"gcm_sender_id": "103953800507",
"prefer_related_applications": false,
"permissions": {
"audio-capture": {
"description": "Required for recording voice messages"
}
}
}
-10826
View File
File diff suppressed because it is too large Load Diff
-115
View File
@@ -1,115 +0,0 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.25.3",
"repository": {
"type": "git",
"url": "git+https://github.com/tinode/webapp.git"
},
"scripts": {
"start": "echo 'Read installation instructions'",
"build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev",
"clean": "rm umd/* && rm css/base.min.css && rm src/i18n.min/*",
"build:css": "postcss css/base.css > css/base.min.css",
"build:dev": "webpack --mode development",
"build:prod": "webpack --mode production",
"build:i18n": "node scripts/i18n.js",
"i18n:missing": "node scripts/i18n.js missing",
"i18n:obsolete": "node scripts/i18n.js obsolete",
"vers": "node scripts/gen-version.js",
"extract": "formatjs extract \"src/**/*.{js,jsx}\" --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"compile": "formatjs compile",
"test": "jest"
},
"browserslist": [
"defaults"
],
"overrides": {
"@babel/core": "^8.0.0"
},
"files": [
"umd/*.js",
"umd/*.map",
"audio/*.m4a",
"css/base.css",
"css/base.min.css",
"img/*.png",
"img/*.svg",
"img/*.jpeg",
"img/bkg/*.jpg",
"img/bkg/*.png",
"img/bkg/*.json",
"index-dev.html",
"index.html",
"manifest.json",
"service-worker.js",
"version.js"
],
"keywords": [
"chat",
"instant-messenger",
"message",
"messenger",
"react",
"web"
],
"locales": [
"ar",
"de",
"en",
"es",
"fr",
"it",
"ko",
"ro",
"ru",
"th",
"uk",
"vi",
"zh",
"zh-TW"
],
"main": "umd/index.prod.js",
"bugs": {
"url": "https://github.com/tinode/webapp/issues"
},
"homepage": "https://github.com/tinode/chat",
"email": "info@tinode.co",
"author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0",
"dependencies": {
"firebase": "^12.15.0",
"jsqr": "^1.4.0",
"libphonenumber-js": "^1.10.40",
"pinyin-pro": "^3.29.3",
"qrcodejs": "^1.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-intl": "^7.0.0",
"tinode-sdk": "^0.25.3",
"webm-duration-fix": "^1.0.4"
},
"devDependencies": {
"@babel/core": "^8.0.0",
"@babel/preset-env": "^8.0.0",
"@babel/preset-react": "^8.0.0",
"@formatjs/cli": "^6.1.3",
"babel-loader": "^10.0.0",
"babel-plugin-formatjs": "^11.0.0",
"browserslist": "^4.21.10",
"copy-webpack-plugin": "^14.0.0",
"cssnano": "^8.0.0",
"glob": "^13.0.0",
"jest": "^30.0.0",
"postcss": "^8.4.27",
"postcss-cli": "^11.0.0",
"webpack": "^5.88.2",
"webpack-cli": "^7.0.0"
},
"allowScripts": {
"@firebase/util@1.15.1": true,
"fsevents@2.3.3": true,
"protobufjs@7.6.5": true,
"unrs-resolver@1.12.2": true
}
}

Some files were not shown because too many files have changed in this diff Show More