remove legacy root Tinode webapp

This commit is contained in:
richjohnrambo
2026-10-14 14:01:52 +08:00
parent ec22b709b5
commit b7f214daf5
443 changed files with 34 additions and 195485 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
-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
.git.worktree-pointer.bak
node_modules
src/i18n/srci/
webapp.code-workspace.code-workspace
.idea/
*.code-workspace
# Local developer overrides — never commit real keys.
.env.local
+4 -9
View File
@@ -1,9 +1,6 @@
# ---- Stage 1: build static assets ----
# The frontend was fully rewritten from scratch under web/ (Vite + React + TS) on branch
# redesign/desktop-multicolumn, merged into dev 2026-09-09 -- this replaces the old webpack build
# 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.
# The active frontend lives under web/ (Vite + React + TypeScript). The legacy
# root-level Webpack application has been removed; production must only build web/.
FROM node:22-alpine AS build
WORKDIR /app/web
@@ -26,10 +23,8 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
WORKDIR /usr/share/nginx/html
# Vite's build output is fully self-contained: index.html + favicon.svg/icons.svg (from web/public/,
# copied verbatim) + assets/ (content-hashed JS/CSS chunks) -- no separate umd/css/img/audio/
# manifest/service-worker/firebase files the way the old build had, none of that machinery exists
# in the new app.
# Vite's build output is fully self-contained: index.html, public assets and
# content-hashed JS/CSS chunks.
COPY --from=build /app/web/dist/. ./
HEALTHCHECK --interval=1m --timeout=3s --start-period=10s \
+27 -77
View File
@@ -1,89 +1,39 @@
# 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="<部署密钥>" -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.
* 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.
测试集群通过 Gitea Actions 的 `Deploy im-webapp (k3s test cluster)` 工作流手动发布。
## Helping out
## 目录
* 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
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.
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>
- `web/`:当前 Vite 前端源码、测试和静态资源
- `docker/`:Nginx 配置
- `docs/`:设计及实施文档
- `.gitea/workflows/`:测试集群部署工作流
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
-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
}
}
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
plugins: [
require('cssnano')({
preset: 'default',
}),
],
};
-11
View File
@@ -1,11 +0,0 @@
# Push notifications
If you want to use the app with your own server and want web push notification to work you have to set them up:
* Register at https://firebase.google.com/, set up the project if you have not done so already.
* Follow instructions to create a web application https://support.google.com/firebase/answer/9326094 in your project.
* Follow instructions at https://support.google.com/firebase/answer/7015592 to get a Firebase configuration object ("Firebase SDK snippet").
* Locate `firebase-init.js` in the root folder of your copy of TinodeWeb app. Copy-paste the following keys from the configuration object to `firebase-init.js`: `apiKey`, `messagingSenderId`, `projectId`, `appId` (you may copy all keys).
* Copy `Web Push certificate` (Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates) to `messagingVapidKey` field in `firebase-init.js`.
* Double check that `firebase-init.js` contains the following keys: `apiKey`, `messagingSenderId`, `projectId`, `appId`, `messagingVapidKey`. The file may contain other optional keys.
* Copy Google-provided server key to `tinode.conf`, see details [here](https://github.com/tinode/chat/blob/master/docs/faq.md#q-how-to-setup-fcm-push-notifications).
-18
View File
@@ -1,18 +0,0 @@
const fs = require('fs');
const version = require('../package.json').version;
function writeVersion(fileName, useExport) {
fs.writeFile(fileName,
'// This is a generated file. Don\'t edit.\n\n'+
`${useExport ? 'export ' : ''}const PACKAGE_VERSION = "${version}";\n`,
{ flag: 'w' },
(err) => {
if (err) {
return console.error(err);
}
}
);
}
writeVersion('./src/version.js', true);
writeVersion('./version.js', false);
-127
View File
@@ -1,127 +0,0 @@
// 1. Updates translations files with new messages, mark no longer needed translations as obsolete.
// 2. Takes all translations and groups them into a single file.
// 3. When called with parameters 'missing' or 'obsolete' writes missing and obsolete translations to console.
const fs = require('fs');
const path = require('path');
const globSync = require('glob').sync;
const EXTRACTED_STRINGS = './src/i18n/ex/base-en.json';
const BASE_LANG = 'en';
const LANG_PATTERN = './src/i18n/*.json';
const LANG_PATTERN_ONE = './src/i18n/%s.json';
const OUTPUT_PATTERN = './src/i18n.min/%s.json';
const args = process.argv.slice(2);
function getLocaleFromFileName(filename) {
const parts = filename.split(path.sep);
return parts[parts.length - 1].split('.json')[0];
}
function printf(str) {
const args = [].slice.call(arguments, 1);
let i = 0;
return str.replace(/%s/g, () => args[i++]);
}
// Called with one or two parameters?
if (args[0] == 'missing' || args[0] == 'obsolete') {
// Extract missing or obsolete translations as requested.
// Read either all or just one translation.
const pattern = args[1] ? printf(LANG_PATTERN_ONE, args[1]) : LANG_PATTERN;
const extracted = globSync(pattern)
.map((filename) => {
const locale = getLocaleFromFileName(filename);
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
const dest = {};
// Copy messages while skipping missing and obsolete ones.
for (let key in translated) {
if ((args[0] == 'missing' && translated[key].missing) ||
(args[0] == 'obsolete' && translated[key].obsolete)) {
dest[key] = translated[key];
}
}
return { [locale]: dest };
})
.reduce((collection, descriptors) => {
return { ...collection, ...descriptors };
}, {});
console.log(JSON.stringify(extracted, null, 2)+"\n");
return;
}
// The formatjs/cli saves extracted messages into a file {'message_id': {defaultMessage: 'default message text', description: '...'}, ...}.
// Rewrite them into a single file en.json {'message_id': 'default message text', }.
// Read master-set of messages in default language.
const baseMessages = JSON.parse(fs.readFileSync(EXTRACTED_STRINGS, 'utf8'));
// Find messages present in baseMessages but missing in translations and vice versa.
// Mark missing or obsolete messages as such.
globSync(LANG_PATTERN)
.map((filename) => {
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
const locale = getLocaleFromFileName(filename);
// Refresh existing entries with possible new descriptions and default messages, add missing.
Object.entries(baseMessages).forEach((ent) => {
const [key, value] = ent;
let translation = translated[key] ? translated[key].translation : "";
if (!translation && locale == BASE_LANG) {
translation = value.defaultMessage;
}
translated[key] = {
translation: translation,
defaultMessage: value.defaultMessage,
description: value.description,
missing: !translation,
obsolete: false
};
});
// Find no longer needed messages.
Object.entries(translated).forEach((ent) => {
const [key, value] = ent;
if (!baseMessages[key] && translated[key]) {
// Obsolete, no longer missing.
translated[key].missing = !translated[key].translation;
translated[key].obsolete = true;
}
});
fs.writeFileSync(filename, JSON.stringify(translated, null, 2)+"\n");
});
// Legacy: Combine all translations into a single json {"lang_1": {...}, "lang_2: {...}"}
// while removing messages marked as missing or obsolete.
// New way: Emit one file per language with all unnecessary data stripped.
// Read all translations.
const messages = globSync(LANG_PATTERN)
.map((filename) => {
const locale = getLocaleFromFileName(filename);
const translated = JSON.parse(fs.readFileSync(filename, 'utf8'));
const dest = {};
// Copy messages while skipping missing and obsolete ones.
for (let key in translated) {
if (!translated[key].missing && !translated[key].obsolete) {
dest[key] = translated[key].translation;
}
}
return { [locale]: dest };
})
.reduce((collection, descriptors) => {
return { ...collection, ...descriptors };
}, {});
// Write minified translation files, one per language.
for (let lang in messages) {
fs.writeFileSync(printf(OUTPUT_PATTERN, lang), JSON.stringify(messages[lang]));
}
-210
View File
@@ -1,210 +0,0 @@
// Must be located at the root.
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-app-compat.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-messaging-compat.js');
importScripts('firebase-init.js');
importScripts('version.js');
// Channel to notify the webapp. There is no BroadcastChannel in Safari.
const webAppChannel = (typeof BroadcastChannel == 'function') ?
new BroadcastChannel('tinode-sw') : null;
// Basic internationalization.
const i18n = {
'ar': {
'new_message': "رسالة جديدة",
'new_chat': "محادثة جديدة",
},
'de': {
'new_message': "Neue Nachricht",
'new_chat': "Neuer Chat",
},
'en': {
'new_message': "New message",
'new_chat': "New chat",
},
'fr': {
'new_message': "Nouveau message",
'new_chat': "Nouvelle conversation",
},
'es': {
'new_message': "Nuevo mensaje",
'new_chat': "Nueva conversación",
},
'it': {
'new_message': "Nuovo Messaggio",
'new_chat': "Nuova Chat",
},
'ko': {
'new_message': "새로운 메시지",
'new_chat': "새로운 채팅",
},
'pt': {
'new_message': "Nova Mensagem",
'new_chat': "Nova Conversa",
},
'ro': {
'new_message': "Mesaj nou",
'new_chat': "Chat nou",
},
'ru': {
'new_message': "Новое сообщение",
'new_chat': "Новый чат",
},
'th': {
'new_message': "ข้อความใหม่",
'new_chat': "แชทใหม่",
},
'uk': {
'new_message': "Нове повідомлення",
'new_chat': "Новий чат",
},
'vi': {
'new_message': "Tin nhắn mới",
'new_chat': "Cuộc trò chuyện mới",
},
'zh': {
'new_message': "新讯息",
'new_chat': "新聊天",
},
'zh-TW': {
'new_message': "新訊息",
'new_chat': "新聊天",
}
};
self.i18nMessage = function (id) {
if (!id) {
return null;
}
// Choose translations: given something like 'de-CH', try 'de-CH' then 'de' then 'en'.
const lang = i18n[self.locale] || i18n[self.baseLocale] || i18n['en'];
// Try finding string by id in the specified language, if missing try English, otherwise use the id itself
// as the last resort.
return lang[id] || i18n['en'][id] || id;
}
firebase.initializeApp(FIREBASE_INIT);
const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage(payload => {
// Notify webapp that a message was received.
if (webAppChannel) {
webAppChannel.postMessage(payload.data);
}
if (payload.data.silent == 'true') {
// No need to show anything.
return;
}
const titles = { 'msg': 'new_message', 'sub': 'new_chat' };
const pushType = payload.data.what || 'msg';
const title = payload.data.title || self.i18nMessage(titles[pushType]);
if (title) {
const options = {
body: payload.data.content || '', // TODO: content for 'sub' should be topic's or user's title.
icon: '/img/logo96.png', // TODO: use topic's or user's avatar (would have to fetch for 'sub', read from db for 'msg').
badge: '/img/badge96.png',
tag: payload.data.topic || undefined,
data: {
topic: payload.data.topic
}
};
return self.registration.showNotification(title, options);
}
});
// Update service worker immediately for both the current client
// and all other active clients.
self.addEventListener('install', _ => {
self.skipWaiting();
});
// This code handles a click on notification: takes
// the user to the browser tab with the chat or opens a new tab.
self.addEventListener('notificationclick', event => {
const data = event.notification.data;
event.notification.close();
if (!data) {
console.info("Missing 'data' in notification", event.notification);
return;
}
// '_' mirrors the placeholder Navigation.composeUrl (src/lib/navigation.js) substitutes for an
// empty path[0] ("no sidepanel view, just open this topic") -- a real empty first segment would
// make the path start with '//', which pushState/URL resolve as a protocol-relative reference
// (host becomes "data.topic") rather than a same-origin path. This file runs outside the webpack
// bundle (importScripts, not an ES module) so it can't import the constant; kept in sync by hand.
const urlPath = '/_/' + data.topic;
event.waitUntil(self.clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then(windowClients => {
let anyClient = null;
for (let i = 0; i < windowClients.length; i++) {
const url = new URL(windowClients[i].url);
if (url.pathname.includes(data.topic)) {
// Found the Tinode tab with the right topic open.
return windowClients[i].focus();
} else {
// This will be the least recently used tab.
anyClient = windowClients[i];
}
}
// Found tab with Tinode on a different topic,
// navigate to the right topic.
if (anyClient) {
const url = new URL(anyClient.url);
url.pathname = urlPath;
return anyClient.focus().then(thisClient => {
return thisClient.navigate(url);
});
}
// Did not find a Tinode browser tab. Open one.
const url = new URL(self.location.origin);
url.pathname = urlPath;
return clients.openWindow(url);
}));
});
// This is needed for 'Add to Home Screen'.
self.addEventListener('fetch', event => {
if (event.request.method != 'GET') {
return;
}
event.respondWith((async _ => {
// Try to find the response in the cache.
const cache = await caches.open(PACKAGE_VERSION);
const reqUrl = new URL(event.request.url);
// Using ignoreSearch=true to read cached images and docs despite different auth signatures.
const cachedResponse = await cache.match(event.request, { ignoreSearch: (self.location.origin == reqUrl.origin) });
if (cachedResponse) {
return cachedResponse;
}
// Not found in cache.
const response = await fetch(event.request);
if (!response || response.status != 200 || response.type != 'basic') {
return response;
}
if (reqUrl.protocol == 'http:' || reqUrl.protocol == 'https:') {
await cache.put(event.request, response.clone());
}
return response;
})());
});
// This code gets the human language from the webapp.
self.addEventListener('message', event => {
const data = JSON.parse(event.data);
// The locale is used for selecting strings in an appropriate language.
self.locale = data.locale || '';
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
});

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