Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df173879fa | ||
|
|
b0d1199fb6 | ||
|
|
c063d056b2 | ||
|
|
d7bfd2f417 | ||
|
|
d2b4cb0440 | ||
|
|
72c4f4e54d | ||
|
|
dd29eaca42 | ||
|
|
8bdb8ed5a5 | ||
|
|
dca34b8b5d | ||
|
|
c30c5c07c7 | ||
|
|
361e23b9ec | ||
|
|
8a26a4ccf9 | ||
|
|
c1338985d4 | ||
|
|
440e29975a | ||
|
|
b7f214daf5 | ||
|
|
ec22b709b5 | ||
|
|
0570f2c5bd | ||
|
|
dd77ba903b | ||
|
|
06df51ca08 | ||
|
|
1c17165c86 | ||
|
|
560269c299 | ||
|
|
7b8216ea70 | ||
|
|
bd61be8a86 | ||
|
|
cdd46c0b62 | ||
|
|
96acc6cb39 |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"presets": ["@babel/preset-react", "@babel/preset-env"],
|
||||
"comments": false
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
umd/* binary
|
||||
package-lock.json binary
|
||||
*.min.css binary
|
||||
src/i18n.min/* binary
|
||||
@@ -1,39 +1,73 @@
|
||||
name: Deploy im-webapp (k3s test cluster)
|
||||
name: Deploy im-webapp production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- pro
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)'
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: production-im-webapp
|
||||
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:
|
||||
deploy:
|
||||
runs-on: deploy-host
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 确定镜像tag
|
||||
id: vars
|
||||
- name: Log in to Gitea Container Registry
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.image_tag }}"
|
||||
[ -z "$TAG" ] && TAG=test
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
set -euo pipefail
|
||||
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u admin --password-stdin
|
||||
|
||||
- name: 登录镜像仓库
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin
|
||||
|
||||
- name: 构建并推送镜像
|
||||
- name: Build and push image
|
||||
env:
|
||||
TINODE_API_KEY: ${{ secrets.TINODE_API_KEY }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
|
||||
TINODE_API_KEY="${TINODE_API_KEY#\'}"
|
||||
TINODE_API_KEY="${TINODE_API_KEY%\'}"
|
||||
docker build \
|
||||
--build-arg TINODE_API_KEY="${{ secrets.TINODE_API_KEY }}" \
|
||||
-t git.118im.com/im_team/im-webapp:${{ steps.vars.outputs.tag }} .
|
||||
docker push git.118im.com/im_team/im-webapp:${{ steps.vars.outputs.tag }}
|
||||
--build-arg TINODE_API_KEY="${TINODE_API_KEY}" \
|
||||
-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: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
kubectl rollout restart deployment/im-webapp -n im-webapp-system
|
||||
kubectl rollout status deployment/im-webapp -n im-webapp-system --timeout=120s
|
||||
set -euo pipefail
|
||||
install -d -m 700 "$HOME/.ssh"
|
||||
KEY_FILE="$(mktemp "$HOME/.ssh/olns-deploy.XXXXXX")"
|
||||
cleanup() {
|
||||
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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -17,6 +14,16 @@ COPY web/. .
|
||||
ARG TINODE_API_KEY=
|
||||
ENV VITE_TINODE_API_KEY=$TINODE_API_KEY
|
||||
|
||||
# The production web UI and IM REST API use different origins. Local development leaves this unset
|
||||
# and continues to use Vite's same-origin /v1/im proxy.
|
||||
ARG TINODE_API_BASE=
|
||||
ENV VITE_TINODE_API_BASE=$TINODE_API_BASE
|
||||
|
||||
# Leave the file origin unset in production so downloads use the current site's /v0/file route.
|
||||
# A build-time override remains available for isolated development deployments.
|
||||
ARG TINODE_FILE_BASE=
|
||||
ENV VITE_TINODE_FILE_BASE=$TINODE_FILE_BASE
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ---- Stage 2: serve with nginx ----
|
||||
@@ -26,10 +33,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 \
|
||||
|
||||
@@ -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.
|
||||
* 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.
|
||||
生产环境通过 `TINODE_FILE_BASE` 固定附件下载入口;本地开发不设置该变量时,继续使用租户解析结果和 Vite 代理。
|
||||
|
||||
## 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
|
||||
|
||||
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/`:测试集群部署工作流
|
||||
|
||||
@@ -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`。
|
||||
- 部署后实测企业码页、登录页、浏览器标题、图标,以及双账号新消息提示音路径。
|
||||
@@ -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"
|
||||
};
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 65 KiB |
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 676 KiB |
|
Before Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 648 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 625 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 611 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 642 KiB |
|
Before Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 548 KiB |
|
Before Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 448 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 938 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -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 +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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||