Showing preview only (744K chars total). Download the full file or copy to clipboard to get everything.
Repository: yinxin630/fiora
Branch: master
Commit: d741c006c5a0
Files: 319
Total size: 669.4 KB
Directory structure:
gitextract_efe0p5be/
├── .dockerignore
├── .eslintignore
├── .eslintrc
├── .github/
│ └── workflows/
│ ├── codeql-analysis.yml
│ ├── lint.yml
│ ├── test.yml
│ └── ts.yml
├── .gitignore
├── .prettierrc
├── .vscode/
│ └── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yaml
├── index.ts
├── jest.config.js
├── jest.setup.js
├── jest.transformer.js
├── lerna.json
├── package.json
├── packages/
│ ├── app/
│ │ ├── .babelrc
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── .watchmanconfig
│ │ ├── App.tsx
│ │ ├── app.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── components/
│ │ │ │ ├── Avatar.tsx
│ │ │ │ ├── BackButton.tsx
│ │ │ │ ├── Expression.tsx
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── Loading.tsx
│ │ │ │ ├── Nofitication.tsx
│ │ │ │ ├── PageContainer.tsx
│ │ │ │ └── Toast.tsx
│ │ │ ├── hooks/
│ │ │ │ └── useStore.tsx
│ │ │ ├── pages/
│ │ │ │ ├── Chat/
│ │ │ │ │ ├── Chat.tsx
│ │ │ │ │ ├── ChatBackButton.tsx
│ │ │ │ │ ├── ChatRightButton.tsx
│ │ │ │ │ ├── ImageMessage.tsx
│ │ │ │ │ ├── Input.tsx
│ │ │ │ │ ├── InviteMessage.tsx
│ │ │ │ │ ├── Message.tsx
│ │ │ │ │ ├── MessageList.tsx
│ │ │ │ │ ├── SystemMessage.tsx
│ │ │ │ │ └── TextMessage.tsx
│ │ │ │ ├── ChatList/
│ │ │ │ │ ├── ChatList.tsx
│ │ │ │ │ ├── ChatListRightButton.tsx
│ │ │ │ │ ├── Linkman.tsx
│ │ │ │ │ └── SelfInfo.tsx
│ │ │ │ ├── GroupInfo/
│ │ │ │ │ └── GroupInfo.tsx
│ │ │ │ ├── GroupProfile/
│ │ │ │ │ └── GroupProfile.tsx
│ │ │ │ ├── LoginSignup/
│ │ │ │ │ ├── Base.tsx
│ │ │ │ │ ├── Login.tsx
│ │ │ │ │ └── Signup.tsx
│ │ │ │ ├── Other/
│ │ │ │ │ ├── Other.tsx
│ │ │ │ │ ├── PrivacyPolicy.tsx
│ │ │ │ │ └── Sponsor.tsx
│ │ │ │ ├── SearchResult/
│ │ │ │ │ └── SearchResult.tsx
│ │ │ │ └── UserInfo/
│ │ │ │ └── UserInfo.tsx
│ │ │ ├── service.ts
│ │ │ ├── socket.ts
│ │ │ ├── state/
│ │ │ │ ├── action.ts
│ │ │ │ ├── reducer.ts
│ │ │ │ └── store.ts
│ │ │ ├── types/
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── redux.ts
│ │ │ │ └── socket.ts
│ │ │ └── utils/
│ │ │ ├── constant.ts
│ │ │ ├── convertMessage.ts
│ │ │ ├── expressions.ts
│ │ │ ├── fetch.ts
│ │ │ ├── getFriendId.ts
│ │ │ ├── getRandomColor.ts
│ │ │ ├── linkman.ts
│ │ │ ├── platform.ts
│ │ │ ├── storage.ts
│ │ │ ├── time.ts
│ │ │ └── uploadFile.ts
│ │ ├── tests/
│ │ │ └── state/
│ │ │ └── reducer.test.ts
│ │ └── tsconfig.json
│ ├── assets/
│ │ └── package.json
│ ├── bin/
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ ├── deleteMessages.ts
│ │ │ ├── deleteTodayRegisteredUsers.ts
│ │ │ ├── deleteUser.ts
│ │ │ ├── doctor.ts
│ │ │ ├── fixUsersAvatar.ts
│ │ │ ├── getUserId.ts
│ │ │ ├── register.ts
│ │ │ └── updateDefaultGroupName.ts
│ │ └── tsconfig.json
│ ├── config/
│ │ ├── client.ts
│ │ ├── package.json
│ │ └── server.ts
│ ├── database/
│ │ ├── mongoose/
│ │ │ ├── index.ts
│ │ │ ├── initMongoDB.ts
│ │ │ └── models/
│ │ │ ├── friend.ts
│ │ │ ├── group.ts
│ │ │ ├── history.ts
│ │ │ ├── message.ts
│ │ │ ├── notification.ts
│ │ │ ├── socket.ts
│ │ │ └── user.ts
│ │ ├── package.json
│ │ ├── redis/
│ │ │ └── initRedis.ts
│ │ └── tsconfig.json
│ ├── docs/
│ │ ├── .gitignore
│ │ ├── babel.config.js
│ │ ├── docs/
│ │ │ ├── API.md
│ │ │ ├── App.md
│ │ │ ├── CHANGELOG.md
│ │ │ ├── Config.md
│ │ │ ├── FAQ.md
│ │ │ ├── Getting-Start.md
│ │ │ ├── INSTALL.md
│ │ │ └── Script.md
│ │ ├── docusaurus.config.js
│ │ ├── i18n/
│ │ │ ├── en/
│ │ │ │ ├── code.json
│ │ │ │ ├── docusaurus-plugin-content-docs/
│ │ │ │ │ └── current/
│ │ │ │ │ ├── API.md
│ │ │ │ │ ├── App.md
│ │ │ │ │ ├── CHANGELOG.md
│ │ │ │ │ ├── Config.md
│ │ │ │ │ ├── FAQ.md
│ │ │ │ │ ├── Getting-Start.md
│ │ │ │ │ ├── INSTALL.md
│ │ │ │ │ └── Script.md
│ │ │ │ └── docusaurus-theme-classic/
│ │ │ │ ├── footer.json
│ │ │ │ └── navbar.json
│ │ │ └── zh-Hans/
│ │ │ ├── code.json
│ │ │ ├── docusaurus-plugin-content-docs/
│ │ │ │ └── current/
│ │ │ │ ├── API.md
│ │ │ │ ├── App.md
│ │ │ │ ├── CHANGELOG.md
│ │ │ │ ├── Config.md
│ │ │ │ ├── FAQ.md
│ │ │ │ ├── Getting-Start.md
│ │ │ │ ├── INSTALL.md
│ │ │ │ └── Script.md
│ │ │ └── docusaurus-theme-classic/
│ │ │ ├── footer.json
│ │ │ └── navbar.json
│ │ ├── package.json
│ │ ├── sidebars.js
│ │ ├── src/
│ │ │ ├── css/
│ │ │ │ └── custom.css
│ │ │ └── pages/
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ └── static/
│ │ └── .nojekyll
│ ├── i18n/
│ │ ├── en-US/
│ │ │ ├── bin.ts
│ │ │ └── index.ts
│ │ ├── node.index.ts
│ │ ├── package.json
│ │ └── zh-CN/
│ │ ├── bin.ts
│ │ └── index.ts
│ ├── server/
│ │ ├── .nodemonrc
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── PrivacyPolicy.html
│ │ │ ├── index.html
│ │ │ └── manifest.json
│ │ ├── src/
│ │ │ ├── app.ts
│ │ │ ├── main.ts
│ │ │ ├── middlewares/
│ │ │ │ ├── frequency.ts
│ │ │ │ ├── isAdmin.ts
│ │ │ │ ├── isLogin.ts
│ │ │ │ ├── registerRoutes.ts
│ │ │ │ └── seal.ts
│ │ │ ├── routes/
│ │ │ │ ├── group.ts
│ │ │ │ ├── history.ts
│ │ │ │ ├── message.ts
│ │ │ │ ├── notification.ts
│ │ │ │ ├── system.ts
│ │ │ │ └── user.ts
│ │ │ └── types/
│ │ │ ├── index.d.ts
│ │ │ └── server.d.ts
│ │ ├── test/
│ │ │ ├── helpers/
│ │ │ │ └── middleware.ts
│ │ │ └── middlewares/
│ │ │ ├── frequency.spec.ts
│ │ │ ├── isAdmin.spec.ts
│ │ │ ├── isLogin.spec.ts
│ │ │ └── seal.spec.ts
│ │ └── tsconfig.json
│ ├── utils/
│ │ ├── compressImage.ts
│ │ ├── const.ts
│ │ ├── convertMessage.ts
│ │ ├── expressions.ts
│ │ ├── getFriendId.ts
│ │ ├── getRandomAvatar.ts
│ │ ├── getRandomColor.ts
│ │ ├── logger.ts
│ │ ├── package.json
│ │ ├── sleep.ts
│ │ ├── socket.ts
│ │ ├── test/
│ │ │ ├── getFriendId.spec.ts
│ │ │ └── url.spec.ts
│ │ ├── time.ts
│ │ ├── ua.ts
│ │ ├── url.ts
│ │ └── xss.ts
│ └── web/
│ ├── .babelrc
│ ├── build/
│ │ ├── webpack.common.js
│ │ ├── webpack.dev.js
│ │ └── webpack.prod.js
│ ├── package.json
│ ├── src/
│ │ ├── App.less
│ │ ├── App.tsx
│ │ ├── components/
│ │ │ ├── Avatar.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── Dialog.less
│ │ │ ├── Dialog.tsx
│ │ │ ├── Dropdown.less
│ │ │ ├── Dropdown.tsx
│ │ │ ├── IconButton.less
│ │ │ ├── IconButton.tsx
│ │ │ ├── Input.less
│ │ │ ├── Input.tsx
│ │ │ ├── Loading.tsx
│ │ │ ├── Menu.tsx
│ │ │ ├── Message.less
│ │ │ ├── Message.tsx
│ │ │ ├── Progress.tsx
│ │ │ ├── Select.tsx
│ │ │ ├── Tabs.tsx
│ │ │ ├── Tooltip.less
│ │ │ └── Tooltip.tsx
│ │ ├── context.ts
│ │ ├── globalStyles.ts
│ │ ├── hooks/
│ │ │ ├── useAction.ts
│ │ │ ├── useAero.ts
│ │ │ ├── useIsLogin.ts
│ │ │ └── useStore.ts
│ │ ├── localStorage.ts
│ │ ├── main.tsx
│ │ ├── modules/
│ │ │ ├── Chat/
│ │ │ │ ├── Chat.less
│ │ │ │ ├── Chat.tsx
│ │ │ │ ├── ChatInput.less
│ │ │ │ ├── ChatInput.tsx
│ │ │ │ ├── CodeEditor.less
│ │ │ │ ├── CodeEditor.tsx
│ │ │ │ ├── Expression.less
│ │ │ │ ├── Expression.tsx
│ │ │ │ ├── GroupManagePanel.less
│ │ │ │ ├── GroupManagePanel.tsx
│ │ │ │ ├── HeaderBar.less
│ │ │ │ ├── HeaderBar.tsx
│ │ │ │ ├── Message/
│ │ │ │ │ ├── CodeDialog.tsx
│ │ │ │ │ ├── CodeMessage.less
│ │ │ │ │ ├── CodeMessage.tsx
│ │ │ │ │ ├── FileMessage.tsx
│ │ │ │ │ ├── ImageMessage.tsx
│ │ │ │ │ ├── InviteMessage.less
│ │ │ │ │ ├── InviteMessageV2.tsx
│ │ │ │ │ ├── Message.less
│ │ │ │ │ ├── Message.tsx
│ │ │ │ │ ├── SystemMessage.tsx
│ │ │ │ │ ├── TextMessage.tsx
│ │ │ │ │ └── UrlMessage.tsx
│ │ │ │ ├── MessageList.less
│ │ │ │ └── MessageList.tsx
│ │ │ ├── FunctionBarAndLinkmanList/
│ │ │ │ ├── CreateGroup.less
│ │ │ │ ├── CreateGroup.tsx
│ │ │ │ ├── FunctionBar.less
│ │ │ │ ├── FunctionBar.tsx
│ │ │ │ ├── FunctionBarAndLinkmanList.less
│ │ │ │ ├── FunctionBarAndLinkmanList.tsx
│ │ │ │ ├── Linkman.less
│ │ │ │ ├── Linkman.tsx
│ │ │ │ ├── LinkmanList.less
│ │ │ │ └── LinkmanList.tsx
│ │ │ ├── GroupInfo.tsx
│ │ │ ├── InfoDialog.less
│ │ │ ├── InviteInfo.tsx
│ │ │ ├── LoginAndRegister/
│ │ │ │ ├── Login.tsx
│ │ │ │ ├── LoginAndRegister.less
│ │ │ │ ├── LoginAndRegister.tsx
│ │ │ │ ├── LoginRegister.less
│ │ │ │ └── Register.tsx
│ │ │ ├── Sidebar/
│ │ │ │ ├── About.less
│ │ │ │ ├── About.tsx
│ │ │ │ ├── Admin.less
│ │ │ │ ├── Admin.tsx
│ │ │ │ ├── Common.less
│ │ │ │ ├── Download.less
│ │ │ │ ├── Download.tsx
│ │ │ │ ├── OnlineStatus.less
│ │ │ │ ├── OnlineStatus.tsx
│ │ │ │ ├── Reward.less
│ │ │ │ ├── Reward.tsx
│ │ │ │ ├── SelfInfo.less
│ │ │ │ ├── SelfInfo.tsx
│ │ │ │ ├── Setting.less
│ │ │ │ ├── Setting.tsx
│ │ │ │ ├── Sidebar.less
│ │ │ │ └── Sidebar.tsx
│ │ │ └── UserInfo.tsx
│ │ ├── service.ts
│ │ ├── socket.ts
│ │ ├── state/
│ │ │ ├── action.ts
│ │ │ ├── reducer.ts
│ │ │ └── store.ts
│ │ ├── styles/
│ │ │ ├── iconfont.less
│ │ │ ├── normalize.less
│ │ │ └── variable.less
│ │ ├── template.html
│ │ ├── themes.ts
│ │ ├── types/
│ │ │ └── index.d.ts
│ │ └── utils/
│ │ ├── fetch.ts
│ │ ├── getRandomHuaji.ts
│ │ ├── inobounce.ts
│ │ ├── notification.ts
│ │ ├── playSound.ts
│ │ ├── readDiskFile.ts
│ │ ├── setCssVariable.ts
│ │ ├── uploadFile.ts
│ │ └── voice.ts
│ ├── test/
│ │ ├── components/
│ │ │ ├── Avatar.spec.tsx
│ │ │ └── Button.spec.tsx
│ │ ├── localStorage.spec.ts
│ │ └── state/
│ │ └── reducer.spec.ts
│ └── tsconfig.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
**/node_module
packages/docs/
packages/web/.linaria-cache/
packages/web/dist/
yarn-error.log
================================================
FILE: .eslintignore
================================================
node_modules/
dist/
public/
build/
docs/
*.d.ts
================================================
FILE: .eslintrc
================================================
{
"extends": ["eslint-config-airbnb", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"createDefaultProgram": true
},
"env": {
"browser": true,
"node": true,
"jest/globals": true
},
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y", "import", "jest"],
"globals": {
"importScripts": true,
"workbox": true,
"__TEST__": true
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
},
"rules": {
"@typescript-eslint/no-unused-vars": 2,
"global-require": 0,
"implicit-arrow-linebreak": 0,
"import/extensions": [
2,
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
],
"indent": [
2,
4,
{
"SwitchCase": 1
}
],
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/interactive-supports-focus": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/no-noninteractive-element-to-interactive-role": 0,
"no-param-reassign": 0,
"no-plusplus": 0,
"no-script-url": 0,
"no-underscore-dangle": 0,
"object-curly-newline": 0,
"react/jsx-filename-extension": [
2,
{
"extensions": [".js", ".jsx", ".tsx"]
}
],
"react/jsx-indent": [2, 4],
"react/jsx-indent-props": [2, 4],
"react/jsx-props-no-spreading": 0,
"react/jsx-one-expression-per-line": 0,
"react/static-property-placement": 0,
"react-hooks/rules-of-hooks": 2,
"react-hooks/exhaustive-deps": 0,
"react/require-default-props": [2, { "ignoreFunctionalComponents": true }],
"import/prefer-default-export": 0,
"prefer-promise-reject-errors": "off"
}
}
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '32 14 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'typescript', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
================================================
FILE: .github/workflows/lint.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Lint Code Style
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x]
steps:
- uses: actions/checkout@master
- uses: bahmutov/npm-install@v1.4.5
- run: yarn lint
================================================
FILE: .github/workflows/test.yml
================================================
name: Unit Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x]
steps:
- uses: actions/checkout@master
- uses: bahmutov/npm-install@v1.4.5
- run: yarn test
================================================
FILE: .github/workflows/ts.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Typescript Type Check
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
ts:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x]
steps:
- uses: actions/checkout@master
- uses: bahmutov/npm-install@v1.4.5
- run: yarn ts-check
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
dist/
coverage/
.idea/
.linaria-cache/
npm-debug.log
yarn-error.log
.eslintcache
lerna-debug.log
.env
packages/server/public/*
!packages/server/public/avatar/
packages/server/public/avatar/*_*.*
!packages/server/public/favicon-96.png
!packages/server/public/favicon-192.png
!packages/server/public/favicon-512.png
!packages/server/public/manifest.json
!packages/server/public/index.html
!packages/server/public/PrivacyPolicy.html
================================================
FILE: .prettierrc
================================================
{
"tabWidth": 4,
"trailingComma": "all",
"singleQuote": true,
"arrowParens": "always",
"printWidth": 80
}
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules/typescript/lib"
}
================================================
FILE: Dockerfile
================================================
FROM node:14
WORKDIR /usr/app/fiora
COPY packages ./packages
COPY package.json tsconfig.json yarn.lock lerna.json ./
RUN touch .env
RUN yarn install
RUN yarn build:web
CMD yarn start
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2015-2021 碎碎酱
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# [Fiora](https://fiora.suisuijiang.com/) · [](https://github.com/yinxin630/fiora/blob/master/LICENSE) [](http://suisuijiang.com) [](http://nodejs.org/download) [](https://github.com/yinxin630/fiora/actions?query=workflow%3A%22Unit+Test%22) [](https://github.com/yinxin630/fiora/actions?query=workflow%3A%22Typescript+Type+Check%22)
Fiora is an interesting open source chat application. It is developed based on [node.js](https://nodejs.org/), [react](https://reactjs.org/) and [socket.io](https://socket.io/) technologies
- **Richness:** Fiora contains backend, frontend, Android and iOS apps
- **Cross Platform:** Fiora is developed with node.js. Supports Windows / Linux / macOS systems
- **Open Source:** Fiora follows the MIT open source license
Online Example: [https://fiora.suisuijiang.com/](https://fiora.suisuijiang.com/)
Documentation: [https://yinxin630.github.io/fiora/](https://yinxin630.github.io/fiora/)
**Other Client**
Vscode Extension: [https://github.com/moonrailgun/fiora-for-vscode](https://github.com/moonrailgun/fiora-for-vscode)
If you are seek for other open-source IM Application which like discord or slack, maybe try out `Tailchat`: https://tailchat.msgbyte.com/
## Features
1. Register an account and log in, it can save your data for a long time
2. Join an existing group or create your own group to communicate with everyone
3. Chat privately with anyone and add them as friends
4. Multiple message types, including text / emoticons / pictures / codes / files / commands, you can also search for emoticons
5. Push notification when you receive a new message, you can customize the notification ringtone, and it can also read the message out
6. Choose the theme you like, and you can set it as any wallpaper and theme color you like
7. Set up an administrator to manage users
## Screenshot
<img src="https://github.com/yinxin630/fiora/raw/master/packages/docs/static/img/screenshots/screenshot-pc.png" alt="PC" style="max-width:800px" />
<img src="https://github.com/yinxin630/fiora/raw/master/packages/docs/static/img/screenshots/screenshot-phone.png" alt="Phone" height="667" style="max-height:667px" />
<img src="https://github.com/yinxin630/fiora/raw/master/packages/docs/static/img/screenshots/screenshot-app.png" alt="App" height="896" style="max-height:896px" />
## Install
Fiora provides two ways to install
- [Install by source code](https://yinxin630.github.io/fiora/docs/install#how-to-run)
- [Install by docker](https://yinxin630.github.io/fiora/docs/install#running-on-the-docker)
## Change Log
You can find the Fiora changelog [on the website](https://yinxin630.github.io/fiora/docs/changelog)
## Contribution
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Please make sure to update tests as appropriate
1. Fork it (<https://github.com/yinxin630/fiora/fork>)
2. Create your feature branch (`git checkout -b some-feature`)
3. Commit your changes (`git commit -am 'Add some some features'`)
4. Push to the branch (`git push origin some-feature`)
5. Create a new Pull Request
## License
Fiora is [MIT licensed](./LICENSE)
================================================
FILE: docker-compose.yaml
================================================
version: '3.2'
services:
mongodb:
image: mongo
restart: always
redis:
image: redis
restart: always
fiora:
build: .
restart: always
ports:
- "9200:9200"
environment:
- Database=mongodb://mongodb/fiora
- RedisHost=redis
================================================
FILE: index.ts
================================================
#!/usr/bin/env ./node_modules/.bin/ts-node
import { program } from 'commander';
import cp from 'child_process';
import i18n from './packages/i18n/node.index';
function exec(commandStr: string) {
const [command, ...args] = commandStr.split(' ');
cp.execFileSync(command, args, { stdio: 'inherit' });
}
program
.command('getUserId <username>')
.description(i18n('getUserIdDescription'))
.action((username: string) => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts getUserId ${username}`,
);
});
program
.command('register <username> <password>')
.description(i18n('registerDescription'))
.action((username: string, password: string) => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts register ${username} ${password}`,
);
});
program
.command('deleteUser <userId>')
.description(i18n('deleteUserDescription'))
.action((userId: string) => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts deleteUser ${userId}`,
);
});
program
.command('fixUsersAvatar [searchValue] [replaceValue]')
.description(i18n('fixUsersAvatarDescription'))
.action((searchValue = '', replaceValue = '') => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts fixUsersAvatar ${searchValue} ${replaceValue}`,
);
});
program
.command('deleteTodayRegisteredUsers')
.description(i18n('deleteTodayRegisteredUsersDescription'))
.action(() => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts deleteTodayRegisteredUsers`,
);
});
program
.command('deleteMessages')
.description(i18n('deleteMessagesDescription'))
.action(() => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts deleteMessages`,
);
});
program
.command('updateDefaultGroupName <newName>')
.description(i18n('updateDefaultGroupNameDescription'))
.action((newName: string) => {
exec(
`npx ts-node --transpile-only packages/bin/index.ts updateDefaultGroupName ${newName}`,
);
});
program
.command('doctor')
.description(i18n('doctorDescription'))
.action(() => {
exec(`npx ts-node --transpile-only packages/bin/index.ts doctor`);
});
program.usage('[command]');
program.parse(process.argv);
================================================
FILE: jest.config.js
================================================
module.exports = {
preset: 'ts-jest',
moduleNameMapper: {
'^.+\\.(css|less|jpg|png|gif|mp3)$': '<rootDir>/jest.transformer.js',
},
collectCoverage: true,
globals: {
'ts-jest': {
isolatedModules: true,
},
__TEST__: true,
},
setupFilesAfterEnv: ['./jest.setup.js'],
collectCoverageFrom: [
'**/*.{ts,tsx}',
'!**/node_modules/**',
'!**/config/**',
'!**/test/helpers/**',
],
};
================================================
FILE: jest.setup.js
================================================
jest.mock('./packages/web/node_modules/linaria', () => ({
css: jest.fn(() => ''),
}));
jest.mock('./packages/database/node_modules/redis', () => jest.requireActual('redis-mock'));
================================================
FILE: jest.transformer.js
================================================
const path = require('path');
module.exports = {
process(src, filename) {
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
},
};
================================================
FILE: lerna.json
================================================
{
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn"
}
================================================
FILE: package.json
================================================
{
"name": "fiora",
"version": "1.0.0",
"description": "An interesting chat application power by socket.io, koa, mongodb and react",
"license": "MIT",
"bin": "index.ts",
"scripts": {
"start": "npx lerna run start --stream",
"dev:server": "npx lerna run dev:server --stream",
"dev:web": "npx lerna run dev:web --stream",
"build:web": "npx lerna run build:web --stream",
"dev:app": "cd packages/app && yarn dev:app && cd ../../",
"build:android": "cd packages/app && yarn build:android && cd ../../",
"build:ios": "cd packages/app && yarn build:ios && cd ../../",
"script": "npx lerna run script --stream",
"dev:docs": "npx lerna run dev:docs --stream",
"build:docs": "npx lerna run build:docs --stream",
"deploy:docs": "npx lerna run deploy:docs --stream",
"lint": "eslint ./ --ext js,jsx,ts,tsx --ignore-pattern .eslintignore --cache --fix",
"test": "jest",
"ts-check": "tsc --noEmit",
"install": "npx lerna bootstrap && yarn link"
},
"engines": {
"node": ">= 14"
},
"author": {
"name": "碎碎酱",
"email": "yinxin630@gmail.com"
},
"repository": {
"type": "git",
"url": "https://github.com/yinxin630/fiora"
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^12.0.0",
"@types/jest": "^24.0.18",
"@types/node": "^15.14.1",
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
"eslint": "^7.30.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^26.1.0",
"lerna": "^4.0.0",
"redis-mock": "^0.56.3",
"ts-jest": "^26.1.3",
"ts-node": "^10.1.0",
"typescript": "^3.8.2"
},
"dependencies": {
"commander": "^8.0.0"
}
}
================================================
FILE: packages/app/.babelrc
================================================
{
"presets": ["babel-preset-expo"]
}
================================================
FILE: packages/app/.eslintrc
================================================
{
"extends": "../../.eslintrc",
"rules": {
"no-use-before-define": "off",
"consistent-return": "off"
}
}
================================================
FILE: packages/app/.gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# expo
.expo/
.expo-shared/
# dependencies
/node_modules
# misc
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: packages/app/.watchmanconfig
================================================
{}
================================================
FILE: packages/app/App.tsx
================================================
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { Provider } from 'react-redux';
import App from './src/App';
import store from './src/state/store';
export default function Main(props: any) {
return (
<Provider store={store}>
<App {...props} />
</Provider>
);
}
================================================
FILE: packages/app/app.json
================================================
{
"expo": {
"privacy": "public",
"name": "fiora",
"icon": "./icon.png",
"version": "1.1.4",
"description": "App for fiora. An online chatroom",
"slug": "fiora",
"scheme": "fiora",
"ios": {
"bundleIdentifier": "com.suisuijiang.fiora",
"buildNumber": "1.1.4",
"infoPlist": {
"LSApplicationQueriesSchemes": ["wxp"],
"CFBundleLocalizations" : ["zh_CN"],
"CFBundleDevelopmentRegion": "zh_CN"
}
},
"android": {
"package": "com.suisuijiang.fiora",
"versionCode": 10,
"useNextNotificationsApi": true
},
"updates": {
"enabled": false
}
}
}
================================================
FILE: packages/app/package.json
================================================
{
"name": "@fiora/app",
"version": "1.0.0",
"license": "MIT",
"private": true,
"main": "./node_modules/expo/AppEntry.js",
"scripts": {
"dev:app": "expo start",
"eject": "expo eject",
"build:android": "expo build:android -t apk",
"build:ios": "expo build:ios -t archive"
},
"dependencies": {
"@expo/vector-icons": "^12.0.5",
"@react-native-async-storage/async-storage": "~1.15.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-toolkit/triangle": "^0.0.1",
"autobind-decorator": "^2.1.0",
"deepmerge": "^4.2.2",
"expo": "^42.0.0",
"expo-constants": "~11.0.1",
"expo-image-picker": "~10.2.2",
"expo-notifications": "~0.12.3",
"expo-web-browser": "~9.2.0",
"immer": "^9.0.6",
"native-base": "^2.4.5",
"prop-types": "^15.6.1",
"randomcolor": "^0.6.2",
"react": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz",
"react-native-dialog": "^6.2.0",
"react-native-gesture-handler": "~1.10.2",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-reanimated": "~2.2.0",
"react-native-router-flux": "^4.3.1",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.4.0",
"react-redux": "^7.2.2",
"redux": "^4.0.0",
"socket.io-client": "^4.1.3"
},
"devDependencies": {
"@types/randomcolor": "^0.5.5",
"@types/react": "^17.0.14",
"@types/react-native": "^0.64.12",
"@types/react-redux": "^7.1.16",
"@types/redux": "^3.6.0",
"@types/socket.io-client": "^3.0.0",
"expo-cli": "^4.7.3",
"jest-expo": "^42.0.0",
"react-test-renderer": "16.3.1"
}
}
================================================
FILE: packages/app/src/App.tsx
================================================
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Scene, Router, Stack, Tabs, Lightbox } from 'react-native-router-flux';
import { Icon, Root } from 'native-base';
import { connect } from 'react-redux';
import ChatList from './pages/ChatList/ChatList';
import Chat from './pages/Chat/Chat';
import Login from './pages/LoginSignup/Login';
import Signup from './pages/LoginSignup/Signup';
import Loading from './components/Loading';
import Other from './pages/Other/Other';
import Notification from './components/Nofitication';
import { State, User } from './types/redux';
import SelfInfo from './pages/ChatList/SelfInfo';
import ChatBackButton from './pages/Chat/ChatBackButton';
import GroupProfile from './pages/GroupProfile/GroupProfile';
import ChatRightButton from './pages/Chat/ChatRightButton';
import UserInfo from './pages/UserInfo/UserInfo';
import ChatListRightButton from './pages/ChatList/ChatListRightButton';
import SearchResult from './pages/SearchResult/SearchResult';
import GroupInfo from './pages/GroupInfo/GroupInfo';
import BackButton from './components/BackButton';
type Props = {
title: string;
primaryColor: string;
isLogin: boolean;
};
function App({ title, primaryColor, isLogin }: Props) {
const primaryColor10 = `rgba(${primaryColor}, 1)`;
const primaryColor8 = `rgba(${primaryColor}, 0.8)`;
const sceneCommonProps = {
hideNavBar: false,
navigationBarStyle: {
backgroundColor: primaryColor10,
borderBottomWidth: 0,
},
navBarButtonColor: '#f9f9f9',
renderLeftButton: () => <BackButton />,
};
return (
<View style={styles.container}>
<Root>
<Router>
<Stack hideNavBar>
<Lightbox>
<Tabs
key="tabs"
hideNavBar
tabBarStyle={{
backgroundColor: primaryColor8,
borderTopWidth: 0,
}}
showLabel={false}
>
<Scene
key="chatlist"
navBarButtonColor="transparent"
component={ChatList}
initial
hideNavBar={!isLogin}
icon={({ focused }) => (
<Icon
name="chatbubble-ellipses-outline"
style={{
fontSize: 24,
color: focused
? 'white'
: '#bbb',
}}
/>
)}
renderLeftButton={() => <SelfInfo />}
renderRightButton={() => (
<ChatListRightButton />
)}
navigationBarStyle={{
backgroundColor: primaryColor10,
borderBottomWidth: 0,
}}
/>
<Scene
key="other"
component={Other}
hideNavBar
title="其它"
icon={({ focused }) => (
<Icon
name="aperture-outline"
style={{
fontSize: 24,
color: focused
? 'white'
: '#bbb',
}}
/>
)}
/>
</Tabs>
</Lightbox>
<Scene
key="chat"
component={Chat}
title="聊天"
getTitle={title}
hideNavBar={false}
navigationBarStyle={{
backgroundColor: primaryColor10,
borderBottomWidth: 0,
}}
navBarButtonColor="#f9f9f9"
renderLeftButton={() => <ChatBackButton />}
renderRightButton={() => <ChatRightButton />}
/>
<Scene
key="login"
component={Login}
title="登录"
{...sceneCommonProps}
/>
<Scene
key="signup"
component={Signup}
title="注册"
{...sceneCommonProps}
/>
<Scene
key="groupProfile"
component={GroupProfile}
title="群组资料"
{...sceneCommonProps}
/>
<Scene
key="userInfo"
component={UserInfo}
title="个人信息"
{...sceneCommonProps}
/>
<Scene
key="groupInfo"
component={GroupInfo}
title="群组信息"
{...sceneCommonProps}
/>
<Scene
key="searchResult"
component={SearchResult}
title="搜索结果"
{...sceneCommonProps}
/>
</Stack>
</Router>
</Root>
<Loading />
<Notification />
</View>
);
}
export default connect((state: State) => ({
primaryColor: state.ui.primaryColor,
isLogin: !!(state.user as User)?._id,
}))(App);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
================================================
FILE: packages/app/src/components/Avatar.tsx
================================================
import React from 'react';
import { getOSSFileUrl } from '../utils/uploadFile';
import Image from './Image';
type Props = {
src: string;
size: number;
};
export default function Avatar({ src, size }: Props) {
const targetUrl = getOSSFileUrl(
src,
`image/resize,w_${size * 2},h_${size * 2}/quality,q_90`,
) as string;
return (
<Image
src={targetUrl}
width={size}
height={size}
style={{ borderRadius: size / 2 }}
/>
);
}
================================================
FILE: packages/app/src/components/BackButton.tsx
================================================
import { View, Icon, Text } from 'native-base';
import React from 'react';
import { TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
type Props = {
text?: string;
};
function BackButton({ text = '' }: Props) {
return (
<TouchableOpacity onPress={() => Actions.pop()}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Icon
name="chevron-back-outline"
style={{ color: 'white', fontSize: 28 }}
/>
<Text
style={{
color: 'white',
fontSize: 16,
fontWeight: 'bold',
}}
>
{text}
</Text>
</View>
</TouchableOpacity>
);
}
export default BackButton;
================================================
FILE: packages/app/src/components/Expression.tsx
================================================
import React from 'react';
import { View } from 'react-native';
import Image from './Image';
import uri from '../assets/images/baidu.png';
type Props = {
size: number;
index: number;
style?: any;
};
export default function Expression({ size, index, style }: Props) {
return (
<View
style={[{ width: size, height: size, overflow: 'hidden' }, style]}
>
<Image
src={uri}
width={size}
height={(size * 3200) / 64}
style={{ marginTop: -size * index }}
/>
</View>
);
}
================================================
FILE: packages/app/src/components/Image.tsx
================================================
import React from 'react';
import { Image as BaseImage, ImageSourcePropType } from 'react-native';
import { getOSSFileUrl } from '../utils/uploadFile';
import { referer } from '../utils/constant';
type Props = {
src: string;
width?: string | number;
height?: string | number;
style?: any;
};
export default function Image({
src,
width = '100%',
height = '100%',
style,
}: Props) {
// @ts-ignore
let source: ImageSourcePropType = src;
if (typeof src === 'string') {
let uri = getOSSFileUrl(src, `image/quality,q_80`);
if (width !== '100%' && height !== '100%') {
uri = getOSSFileUrl(
src,
`image/resize,w_${Math.ceil(width as number)},h_${Math.ceil(
height as number,
)}/quality,q_80`,
);
}
source = {
uri: uri as string,
cache: 'force-cache',
headers: {
Referer: referer,
},
};
}
return <BaseImage source={source} style={[style, { width, height }]} />;
}
================================================
FILE: packages/app/src/components/Loading.tsx
================================================
import React from 'react';
import { View, Text, Dimensions, StyleSheet } from 'react-native';
import { Spinner } from 'native-base';
import { useStore } from '../hooks/useStore';
const { width: ScreenWidth, height: ScreenHeight } = Dimensions.get('window');
export default function Loading() {
const { loading } = useStore().ui;
if (!loading) {
return null;
}
return (
<View style={styles.loadingView}>
<View style={styles.loadingBox}>
<Spinner color="white" />
<Text style={styles.loadingText}>{loading}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
loadingView: {
width: ScreenWidth,
height: ScreenHeight,
position: 'absolute',
backgroundColor: 'rgba(0,0,0,0.15)',
alignItems: 'center',
justifyContent: 'center',
},
loadingBox: {
width: 120,
height: 120,
backgroundColor: 'rgba(0,0,0,0.7)',
borderRadius: 10,
alignItems: 'center',
},
loadingText: {
color: 'white',
},
});
================================================
FILE: packages/app/src/components/Nofitication.tsx
================================================
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { useState, useEffect } from 'react';
import { Platform, AppState } from 'react-native';
import { Actions } from 'react-native-router-flux';
import { setNotificationToken } from '../service';
import action from '../state/action';
import { State, User } from '../types/redux';
import { isiOS } from '../utils/platform';
import { useIsLogin, useStore } from '../hooks/useStore';
import store from '../state/store';
function enableNotification() {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
}
function disableNotification() {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: false,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
}
function Nofitication() {
const isLogin = useIsLogin();
const state = useStore();
const notificationTokens = (state.user as User)?.notificationTokens || [];
const { connect } = state;
const [notificationToken, updateNotificationToken] = useState('');
async function registerForPushNotificationsAsync() {
// Push notification to Android device need google service
// Not supported in China
if (Constants.isDevice && isiOS) {
const {
status: existingStatus,
} = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const {
status,
} = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return;
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
updateNotificationToken(token);
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
}
}
function handleClickNotification(response: any) {
const { focus } = response.notification.request.content.data;
setTimeout(() => {
const currentState = store.getState() as State;
const linkmans = currentState.linkmans || [];
if (linkmans.find((linkman) => linkman._id === focus)) {
action.setFocus(focus);
if (Actions.currentScene !== 'chat') {
Actions.chat();
}
}
}, 1000);
}
useEffect(() => {
disableNotification();
registerForPushNotificationsAsync();
Notifications.addNotificationResponseReceivedListener(
handleClickNotification,
);
}, []);
useEffect(() => {
if (
connect &&
isLogin &&
notificationToken &&
!notificationTokens.includes(notificationToken)
) {
setNotificationToken(notificationToken);
}
}, [connect, isLogin, notificationToken]);
function handleAppStateChange(nextAppState: string) {
if (nextAppState === 'active') {
disableNotification();
} else if (nextAppState === 'background') {
enableNotification();
}
}
useEffect(() => {
AppState.addEventListener('change', handleAppStateChange);
return () => {
AppState.removeEventListener('change', handleAppStateChange);
};
}, []);
return null;
}
export default Nofitication;
================================================
FILE: packages/app/src/components/PageContainer.tsx
================================================
import { View } from 'native-base';
import React from 'react';
import { ImageBackground, SafeAreaView, StyleSheet } from 'react-native';
type Props = {
children: any;
disableSafeAreaView?: boolean;
};
function PageContainer({ children, disableSafeAreaView = false }: Props) {
return (
<ImageBackground
source={require('../assets/images/background-cool.jpg')}
style={styles.backgroundImage}
blurRadius={10}
>
<View style={styles.children}>
{disableSafeAreaView ? (
children
) : (
<SafeAreaView style={[styles.container]}>
{children}
</SafeAreaView>
)}
</View>
</ImageBackground>
);
}
export default PageContainer;
const styles = StyleSheet.create({
container: {
flex: 1,
},
backgroundImage: {
flex: 1,
resizeMode: 'cover',
},
children: {
flex: 1,
backgroundColor: 'rgba(241, 241, 241, 0.6)',
},
});
================================================
FILE: packages/app/src/components/Toast.tsx
================================================
import { Toast } from 'native-base';
export default {
success(message: string) {
Toast.show({
text: message,
type: 'success',
position: 'top',
});
},
warning(message: string) {
Toast.show({
text: message,
type: 'warning',
position: 'top',
});
},
danger(message: string) {
Toast.show({
text: message,
type: 'danger',
position: 'top',
});
},
};
================================================
FILE: packages/app/src/hooks/useStore.tsx
================================================
import { useSelector } from 'react-redux';
import { State, User } from '../types/redux';
export function useStore() {
return useSelector((state: State) => state);
}
export function useUser() {
return useStore().user as User;
}
export function useSelfId() {
const user = useUser();
return (user && user._id) || '';
}
export function useIsLogin() {
return !!useSelfId();
}
export function useIsAdmin() {
const user = useUser();
return (user && user.isAdmin) || false;
}
export function useTheme() {
const { ui } = useStore();
const { primaryColor, primaryTextColor } = ui;
return {
primaryColor8: `rgba(${primaryColor}, 0.8)`,
primaryColor10: `rgba(${primaryColor}, 1)`,
primaryTextColor10: `rgba(${primaryTextColor}, 1)`,
};
}
export function useLinkmans() {
const data = useStore();
return data.linkmans || [];
}
export function useFocusLinkman() {
const data = useStore();
const { linkmans, focus = '' } = data;
if (linkmans) {
return linkmans.find((linkman) => linkman._id === focus);
}
return null;
}
export function useFocus() {
const data = useStore();
return data.focus || '';
}
================================================
FILE: packages/app/src/pages/Chat/Chat.tsx
================================================
import React, { useEffect, useRef } from 'react';
import {
StyleSheet,
KeyboardAvoidingView,
ScrollView,
Dimensions,
} from 'react-native';
import Constants from 'expo-constants';
import { Actions } from 'react-native-router-flux';
import { isiOS } from '../../utils/platform';
import MessageList from './MessageList';
import Input from './Input';
import PageContainer from '../../components/PageContainer';
import { Friend, Group, Linkman } from '../../types/redux';
import {
useFocusLinkman,
useIsLogin,
useSelfId,
useStore,
} from '../../hooks/useStore';
import {
getDefaultGroupOnlineMembers,
getGroupOnlineMembers,
getUserOnlineStatus,
} from '../../service';
import action from '../../state/action';
import { formatLinkmanName } from '../../utils/linkman';
import fetch from '../../utils/fetch';
let lastMessageIdCache = '';
const keyboardOffset = (() => {
const { width, height } = Dimensions.get('window');
const screenRatio = height / width;
if (screenRatio === 667 / 375) {
// iPhone 6 / 7 / 8
return 64;
}
if (screenRatio === 736 / 414) {
// iPhone 6 / 7 / 8 PLUS
return 64;
}
if (screenRatio === 812 / 375) {
// iPhone X / 12mini
return 86;
}
if (screenRatio === 896 / 414) {
// iPhone Xr / 11 / 11 Pro Max
return 86;
}
if (screenRatio === 844 / 390) {
// iPhone 12 / 12 Prop
return 64;
}
if (screenRatio === 926 / 428) {
// iPhone 12 Pro Max
return 64;
}
return Constants.statusBarHeight + 44;
})();
export default function Chat() {
const isLogin = useIsLogin();
const self = useSelfId();
const { focus } = useStore();
const linkman = useFocusLinkman();
const $messageList = useRef<ScrollView>();
async function fetchGroupOnlineMembers() {
let onlineMembers: Group['members'] = [];
if (isLogin) {
onlineMembers = await getGroupOnlineMembers(focus);
} else {
onlineMembers = await getDefaultGroupOnlineMembers();
}
if (onlineMembers) {
action.updateGroupProperty(focus, 'members', onlineMembers);
}
}
async function fetchUserOnlineStatus() {
const isOnline = await getUserOnlineStatus(focus.replace(self, ''));
action.updateFriendProperty(focus, 'isOnline', isOnline);
}
useEffect(() => {
if (!linkman || !isLogin) {
return;
}
const request =
linkman.type === 'group'
? fetchGroupOnlineMembers
: fetchUserOnlineStatus;
request();
const timer = setInterval(() => request(), 1000 * 60);
return () => clearInterval(timer);
}, [focus, isLogin]);
useEffect(() => {
if (Actions.currentScene !== 'chat') {
return;
}
Actions.refresh({
title: formatLinkmanName(linkman as Linkman),
});
}, [(linkman as Group).members, (linkman as Friend).isOnline]);
async function intervalUpdateHistory() {
if (isLogin && linkman) {
if (linkman.messages.length > 0) {
const lastMessageId =
linkman.messages[linkman.messages.length - 1]._id;
if (lastMessageId !== lastMessageIdCache) {
lastMessageIdCache = lastMessageId;
await fetch('updateHistory', {
linkmanId: focus,
messageId: lastMessageId,
});
}
}
}
}
useEffect(() => {
const timer = setInterval(intervalUpdateHistory, 1000 * 5);
return () => clearInterval(timer);
}, [focus]);
function scrollToEnd(time = 0) {
if (time > 200) {
return;
}
if ($messageList.current) {
$messageList.current!.scrollToEnd({ animated: false });
}
setTimeout(() => {
scrollToEnd(time + 50);
}, 50);
}
function handleInputHeightChange() {
if ($messageList.current) {
scrollToEnd();
}
}
return (
<PageContainer disableSafeAreaView>
<KeyboardAvoidingView
style={styles.container}
behavior={isiOS ? 'padding' : 'height'}
keyboardVerticalOffset={keyboardOffset}
>
{/*
// @ts-ignore */}
<MessageList $scrollView={$messageList} />
<Input onHeightChange={handleInputHeightChange} />
</KeyboardAvoidingView>
</PageContainer>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
================================================
FILE: packages/app/src/pages/Chat/ChatBackButton.tsx
================================================
import React from 'react';
import BackButton from '../../components/BackButton';
import { useStore } from '../../hooks/useStore';
function ChatBackButton() {
const store = useStore();
const unread = store.linkmans.reduce((result, linkman) => {
result += linkman.unread;
return result;
}, 0);
return <BackButton text={unread.toString()} />;
}
export default ChatBackButton;
================================================
FILE: packages/app/src/pages/Chat/ChatRightButton.tsx
================================================
import { View, Icon } from 'native-base';
import React from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
import { useFocusLinkman } from '../../hooks/useStore';
function ChatRightButton() {
const linkman = useFocusLinkman();
function handleClick() {
if (linkman?.type === 'group') {
Actions.push('groupProfile');
} else {
Actions.push('userInfo', { user: linkman });
}
}
return (
<TouchableOpacity onPress={handleClick}>
<View style={styles.container}>
<Icon name="ellipsis-horizontal" style={styles.icon} />
</View>
</TouchableOpacity>
);
}
export default ChatRightButton;
const styles = StyleSheet.create({
container: {
width: 44,
height: 44,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
icon: {
color: 'white',
fontSize: 26,
},
});
================================================
FILE: packages/app/src/pages/Chat/ImageMessage.tsx
================================================
/* eslint-disable react/jsx-props-no-spreading */
import { View } from 'native-base';
import React from 'react';
import { Dimensions, StyleSheet, TouchableOpacity } from 'react-native';
import Image from '../../components/Image';
import { Message } from '../../types/redux';
const { width: ScreenWidth } = Dimensions.get('window');
type Props = {
message: Message;
openImageViewer: (imageUrl: string) => void;
couldDelete: boolean;
onLongPress: () => void;
};
function ImageMessage({
message,
openImageViewer,
couldDelete,
onLongPress,
}: Props) {
const maxWidth = ScreenWidth - 130 - 16;
const maxHeight = 200;
let scale = 1;
let width = 0;
let height = 0;
const parseResult = /width=([0-9]+)&height=([0-9]+)/.exec(message.content);
if (parseResult) {
width = parseInt(parseResult[1], 10);
height = parseInt(parseResult[2], 10);
if (width * scale > maxWidth) {
scale = maxWidth / width;
}
if (height * scale > maxHeight) {
scale = maxHeight / height;
}
}
function handleImageClick() {
const imageUrl = message.content;
openImageViewer(imageUrl);
}
return (
<View
style={[
styles.container,
{ width: width * scale, height: height * scale },
]}
>
<TouchableOpacity
onPress={handleImageClick}
{...(couldDelete ? { onLongPress } : {})}
>
<Image
src={message.content}
style={{ width: width * scale, height: height * scale }}
/>
</TouchableOpacity>
</View>
);
}
export default ImageMessage;
const styles = StyleSheet.create({
container: {
height: 200,
width: ScreenWidth - 130 - 16,
borderRadius: 3,
overflow: 'hidden',
},
});
================================================
FILE: packages/app/src/pages/Chat/Input.tsx
================================================
import React, { useRef, useState } from 'react';
import {
StyleSheet,
View,
TextInput,
Text,
Dimensions,
TouchableOpacity,
SafeAreaView,
} from 'react-native';
import { Button } from 'native-base';
import { Actions } from 'react-native-router-flux';
import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import action from '../../state/action';
import fetch from '../../utils/fetch';
import { isiOS } from '../../utils/platform';
import expressions from '../../utils/expressions';
import Expression from '../../components/Expression';
import { useIsLogin, useStore, useUser } from '../../hooks/useStore';
import { Message } from '../../types/redux';
import uploadFile from '../../utils/uploadFile';
const { width: ScreenWidth } = Dimensions.get('window');
const ExpressionSize = (ScreenWidth - 16) / 10;
type Props = {
onHeightChange: () => void;
};
export default function Input({ onHeightChange }: Props) {
const isLogin = useIsLogin();
const user = useUser();
const { focus } = useStore();
const [message, setMessage] = useState('');
const [showFunctionList, toggleShowFunctionList] = useState(true);
const [showExpression, toggleShowExpression] = useState(false);
const [cursorPosition, setCursorPosition] = useState({ start: 0, end: 0 });
const $input = useRef<TextInput>();
function setInputText(text = '') {
// iossetNativeProps无效, 解决办法参考:https://github.com/facebook/react-native/issues/18272
if (isiOS) {
$input.current!.setNativeProps({ text: text || ' ' });
}
setTimeout(() => {
$input.current!.setNativeProps({ text: text || '' });
});
}
function addSelfMessage(type: string, content: string) {
const _id = focus + Date.now();
const newMessage: Message = {
_id,
type,
content,
createTime: Date.now(),
from: {
_id: user._id,
username: user.username,
avatar: user.avatar,
tag: user.tag,
},
to: '',
loading: true,
};
if (type === 'image') {
newMessage.percent = 0;
}
action.addLinkmanMessage(focus, newMessage);
return _id;
}
async function sendMessage(localId: string, type: string, content: string) {
const [err, res] = await fetch('sendMessage', {
to: focus,
type,
content,
});
if (!err) {
res.loading = false;
action.updateSelfMessage(focus, localId, res);
}
}
function handleSubmit() {
if (message === '') {
return;
}
const id = addSelfMessage('text', message);
sendMessage(id, 'text', message);
setMessage('');
toggleShowFunctionList(true);
toggleShowExpression(false);
setInputText();
}
function handleSelectionChange(event: any) {
const { start, end } = event.nativeEvent.selection;
setCursorPosition({
start,
end,
});
}
function handleFocus() {
toggleShowFunctionList(true);
toggleShowExpression(false);
}
function openExpression() {
$input.current!.blur();
toggleShowFunctionList(false);
toggleShowExpression(true);
onHeightChange();
}
async function handleClickImage() {
const currentPermission = await ImagePicker.getMediaLibraryPermissionsAsync();
if (currentPermission.accessPrivileges === 'none') {
if (currentPermission.canAskAgain) {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permission.accessPrivileges === 'none') {
return;
}
} else {
return;
}
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
base64: true,
});
if (!result.cancelled) {
const id = addSelfMessage(
'image',
`${result.uri}?width=${result.width}&height=${result.height}`,
);
const key = `ImageMessage/${user._id}_${Date.now()}`;
const imageUrl = await uploadFile(
result.base64 as string,
key,
true,
);
sendMessage(
id,
'image',
`${imageUrl}?width=${result.width}&height=${result.height}`,
);
}
}
async function handleClickCamera() {
const currentPermission = await ImagePicker.getCameraPermissionsAsync();
if (currentPermission.status === 'undetermined') {
if (currentPermission.canAskAgain) {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (permission.status === 'undetermined') {
return;
}
} else {
return;
}
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
base64: true,
});
if (!result.cancelled) {
const id = addSelfMessage(
'image',
`${result.uri}?width=${result.width}&height=${result.height}`,
);
const key = `ImageMessage/${user._id}_${Date.now()}`;
const imageUrl = await uploadFile(
result.base64 as string,
key,
true,
);
sendMessage(
id,
'image',
`${imageUrl}?width=${result.width}&height=${result.height}`,
);
}
}
function handleChangeText(value: string) {
setMessage(value);
}
function insertExpression(e: string) {
const expression = `#(${e})`;
const newValue = `${message.substring(
0,
cursorPosition.start,
)}${expression}${message.substring(
cursorPosition.end,
message.length,
)}`;
setMessage(newValue);
setCursorPosition({
start: cursorPosition.start + expression.length,
end: cursorPosition.start + expression.length,
});
setInputText(newValue);
}
return (
<SafeAreaView style={styles.safeView}>
<View style={styles.container}>
{isLogin ? (
<View style={styles.inputContainer}>
<TextInput
// @ts-ignore
ref={$input}
style={styles.input}
placeholder="随便聊点啥吧, 不要无意义刷屏~~"
onChangeText={handleChangeText}
onSubmitEditing={handleSubmit}
autoCapitalize="none"
blurOnSubmit={false}
maxLength={2048}
returnKeyType="send"
enablesReturnKeyAutomatically
underlineColorAndroid="transparent"
onSelectionChange={handleSelectionChange}
onFocus={handleFocus}
/>
</View>
) : (
<Button block style={styles.button} onPress={Actions.login}>
<Text style={styles.buttonText}>
登录 / 注册, 参与聊天
</Text>
</Button>
)}
{isLogin && showFunctionList ? (
<View style={styles.iconButtonContainer}>
<Button
transparent
style={styles.iconButton}
onPress={openExpression}
>
<Ionicons name="ios-happy" size={28} color="#999" />
</Button>
<Button
transparent
style={styles.iconButton}
onPress={handleClickImage}
>
<Ionicons name="ios-image" size={28} color="#999" />
</Button>
<Button
transparent
style={styles.iconButton}
onPress={handleClickCamera}
>
<Ionicons
name="ios-camera"
size={28}
color="#999"
/>
</Button>
</View>
) : null}
{showExpression ? (
<View style={styles.expressionContainer}>
{expressions.default.map((e, i) => (
<TouchableOpacity
key={e}
onPress={() => insertExpression(e)}
>
<View style={styles.expression}>
<Expression index={i} size={30} />
</View>
</TouchableOpacity>
))}
</View>
) : null}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeView: {
backgroundColor: 'rgba(255, 255, 255, 0.5)',
},
container: {
paddingTop: 4,
},
inputContainer: {
flexDirection: 'row',
paddingLeft: 10,
paddingRight: 10,
},
input: {
flex: 1,
height: 36,
paddingLeft: 8,
paddingRight: 8,
backgroundColor: 'white',
borderWidth: 1,
borderColor: '#e5e5e5',
borderRadius: 5,
},
sendButton: {
width: 50,
height: 36,
marginLeft: 8,
paddingLeft: 10,
},
button: {
height: 36,
marginTop: 4,
marginLeft: 10,
marginRight: 10,
marginBottom: 8,
},
buttonText: {
color: 'white',
},
iconContainer: {
height: 40,
},
icon: {
transform: [
{
// @ts-ignore
translate: [0, -3],
},
],
},
iconButtonContainer: {
flexDirection: 'row',
paddingLeft: 15,
paddingRight: 15,
height: 44,
},
iconButton: {
width: '15%',
},
cancelButton: {
borderTopWidth: 1,
borderTopColor: '#e6e6e6',
},
cancelButtonText: {
color: '#666',
},
// 表情框
expressionContainer: {
height: (isiOS ? 34 : 30) * 5 + 6,
flexDirection: 'row',
flexWrap: 'wrap',
paddingTop: 3,
paddingBottom: 3,
paddingLeft: 8,
paddingRight: 8,
},
expression: {
width: ExpressionSize,
height: isiOS ? 34 : 30,
alignItems: 'center',
justifyContent: 'center',
},
});
================================================
FILE: packages/app/src/pages/Chat/InviteMessage.tsx
================================================
import { View, Text } from 'native-base';
import React from 'react';
import { StyleSheet, TouchableNativeFeedback } from 'react-native';
import { Actions } from 'react-native-router-flux';
import Toast from '../../components/Toast';
import { getLinkmanHistoryMessages, joinGroup } from '../../service';
import action from '../../state/action';
import { Message } from '../../types/redux';
type Props = {
message: Message;
isSelf: boolean;
};
function InviteMessage({ message, isSelf }: Props) {
const invite = JSON.parse(message.content);
async function handleJoinGroup() {
const group = await joinGroup(invite.group);
if (group) {
group.type = 'group';
action.addLinkman(group, true);
Actions.refresh({ title: group.name });
Toast.success('加入群组成功');
const messages = await getLinkmanHistoryMessages(invite.group, 0);
if (messages) {
action.addLinkmanHistoryMessages(invite.group, messages);
}
}
}
return (
<TouchableNativeFeedback onPress={handleJoinGroup}>
<View style={styles.container}>
<View
style={[
styles.info,
{ borderBottomColor: isSelf ? 'white' : '#aaa' },
]}
>
<Text style={styles.text}>
"
{invite.inviterName}
" 邀请你加入群组「
{invite.groupName}」
</Text>
</View>
<View style={styles.join}>
<Text style={styles.text}>加入</Text>
</View>
</View>
</TouchableNativeFeedback>
);
}
export default InviteMessage;
const styles = StyleSheet.create({
container: {
width: '90%',
alignItems: 'center',
},
text: {
fontSize: 14,
textAlign: 'center',
lineHeight: 16,
},
info: {
width: '100%',
borderBottomWidth: 1,
borderBottomColor: 'white',
paddingBottom: 4,
},
join: {
width: '100%',
paddingTop: 4,
paddingBottom: 2,
},
});
================================================
FILE: packages/app/src/pages/Chat/Message.tsx
================================================
import React, { useEffect } from 'react';
import {
View,
Text,
StyleSheet,
Dimensions,
TouchableOpacity,
} from 'react-native';
import Triangle from '@react-native-toolkit/triangle';
import { ActionSheet } from 'native-base';
import { Actions } from 'react-native-router-flux';
import Time from '../../utils/time';
import Avatar from '../../components/Avatar';
import { Message as MessageType } from '../../types/redux';
import SystemMessage from './SystemMessage';
import ImageMessage from './ImageMessage';
import TextMessage from './TextMessage';
import { getRandomColor } from '../../utils/getRandomColor';
import InviteMessage from './InviteMessage';
import {
useFocus,
useIsAdmin,
useSelfId,
useTheme,
} from '../../hooks/useStore';
import { deleteMessage } from '../../service';
import action from '../../state/action';
const { width: ScreenWidth } = Dimensions.get('window');
type Props = {
message: MessageType;
isSelf: boolean;
shouldScroll: boolean;
scrollToEnd: () => void;
openImageViewer: (imageUrl: string) => void;
};
function Message({
message,
isSelf,
shouldScroll,
scrollToEnd,
openImageViewer,
}: Props) {
const { primaryColor8 } = useTheme();
const isAdmin = useIsAdmin();
const self = useSelfId();
const focus = useFocus();
const couldDelete =
message.type !== 'system' && (isAdmin || message.from._id === self);
useEffect(() => {
if (shouldScroll) {
scrollToEnd();
}
}, []);
async function handleDeleteMessage() {
const options = ['撤回', '取消'];
ActionSheet.show(
{
options: ['确定', '取消'],
cancelButtonIndex: options.findIndex(
(option) => option === '取消',
),
title: '是否撤回消息?',
},
async (buttonIndex) => {
switch (buttonIndex) {
case 0: {
const isSuccess = await deleteMessage(message._id);
if (isSuccess) {
action.deleteLinkmanMessage(focus, message._id);
}
break;
}
default: {
break;
}
}
},
);
}
function formatTime() {
const createTime = new Date(message.createTime);
const nowTime = new Date();
if (Time.isToday(nowTime, createTime)) {
return Time.getHourMinute(createTime);
}
if (Time.isYesterday(nowTime, createTime)) {
return `昨天 ${Time.getHourMinute(createTime)}`;
}
if (Time.isSameYear(nowTime, createTime)) {
return `${Time.getMonthDate(createTime)} ${Time.getHourMinute(
createTime,
)}`;
}
return `${Time.getYearMonthDate(createTime)} ${Time.getHourMinute(
createTime,
)}`;
}
function handleClickAvatar() {
Actions.push('userInfo', { user: message.from });
}
function renderContent() {
switch (message.type) {
case 'text': {
return <TextMessage message={message} isSelf={isSelf} />;
}
case 'image': {
return (
<ImageMessage
message={message}
openImageViewer={openImageViewer}
couldDelete={couldDelete}
onLongPress={handleDeleteMessage}
/>
);
}
case 'system': {
return <SystemMessage message={message} />;
}
case 'inviteV2': {
return <InviteMessage message={message} isSelf={isSelf} />;
}
case 'file':
case 'code': {
return (
<Text style={{ color: isSelf ? 'white' : '#666' }}>
暂未支持的消息类型[
{message.type}
], 请在Web端查看
</Text>
);
}
default:
return (
<Text style={{ color: isSelf ? 'white' : '#666' }}>
不支持的消息类型
</Text>
);
}
}
return (
<View style={[styles.container, isSelf && styles.containerSelf]}>
{isSelf ? (
<Avatar src={message.from.avatar} size={44} />
) : (
<TouchableOpacity onPress={handleClickAvatar}>
<Avatar src={message.from.avatar} size={44} />
</TouchableOpacity>
)}
<View style={[styles.info, isSelf && styles.infoSelf]}>
<View style={[styles.nickTime, isSelf && styles.nickTimeSelf]}>
{!!message.from.tag && (
<View
style={[
styles.tag,
{
backgroundColor: getRandomColor(
message.from.tag,
),
},
]}
>
<Text style={styles.tagText}>
{message.from.tag}
</Text>
</View>
)}
<Text
style={[
styles.nick,
isSelf ? styles.nickSelf : styles.nickOther,
]}
>
{message.from.username}
</Text>
<Text style={[styles.time, isSelf && styles.timeSelf]}>
{formatTime()}
</Text>
</View>
{couldDelete ? (
<TouchableOpacity onLongPress={handleDeleteMessage}>
<View
style={[
styles.content,
{
backgroundColor: isSelf
? primaryColor8
: 'white',
},
]}
>
{renderContent()}
</View>
</TouchableOpacity>
) : (
<View
style={[
styles.content,
{
backgroundColor: isSelf
? primaryColor8
: 'white',
},
]}
>
{renderContent()}
</View>
)}
<View
style={[
styles.triangle,
isSelf ? styles.triangleSelf : styles.triangleOther,
]}
>
<Triangle
type="isosceles"
mode={isSelf ? 'right' : 'left'}
base={10}
height={5}
color={isSelf ? primaryColor8 : 'white'}
/>
</View>
</View>
</View>
);
}
export default React.memo(Message);
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginBottom: 6,
paddingLeft: 8,
paddingRight: 8,
},
containerSelf: {
flexDirection: 'row-reverse',
},
info: {
position: 'relative',
marginLeft: 8,
marginRight: 8,
maxWidth: ScreenWidth - 120,
alignItems: 'flex-start',
},
infoSelf: {
alignItems: 'flex-end',
},
nickTime: {
flexDirection: 'row',
},
nickTimeSelf: {
flexDirection: 'row-reverse',
},
nick: {
fontSize: 13,
color: '#333',
},
nickSelf: {
marginRight: 4,
},
nickOther: {
marginLeft: 4,
},
time: {
fontSize: 12,
color: '#666',
marginLeft: 4,
},
timeSelf: {
marginRight: 4,
},
content: {
marginTop: 3,
borderRadius: 6,
padding: 5,
paddingLeft: 8,
paddingRight: 8,
backgroundColor: 'white',
minHeight: 26,
minWidth: 20,
marginBottom: 6,
},
triangle: {
position: 'absolute',
top: 25,
},
triangleSelf: {
right: -5,
},
triangleOther: {
left: -5,
},
tag: {
height: 14,
alignItems: 'center',
justifyContent: 'center',
paddingLeft: 3,
paddingRight: 3,
borderRadius: 3,
},
tagText: {
fontSize: 11,
color: 'white',
},
});
================================================
FILE: packages/app/src/pages/Chat/MessageList.tsx
================================================
import React, { useEffect, useState } from 'react';
import { ScrollView, StyleSheet, Keyboard, Modal, Image } from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import action from '../../state/action';
import fetch from '../../utils/fetch';
import Message from './Message';
import {
useFocusLinkman,
useIsLogin,
useSelfId,
useStore,
} from '../../hooks/useStore';
import { Message as MessageType } from '../../types/redux';
import Toast from '../../components/Toast';
import { isAndroid, isiOS } from '../../utils/platform';
import { referer } from '../../utils/constant';
type Props = {
$scrollView: React.MutableRefObject<ScrollView>;
};
let prevContentHeight = 0;
let prevMessageCount = 0;
let shouldScroll = true;
let isFirstTimeFetchHistory = true;
function MessageList({ $scrollView }: Props) {
const isLogin = useIsLogin();
const self = useSelfId();
const focusLinkman = useFocusLinkman();
const { focus } = useStore();
const messages = focusLinkman?.messages || [];
const [refreshing, setRefreshing] = useState(false);
const [showImageViewerDialog, toggleShowImageViewerDialog] = useState(
false,
);
const [imageViewerIndex, setImageViewerIndex] = useState(0);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener(
'keyboardWillShow',
handleKeyboardShow,
);
return () => {
prevContentHeight = 0;
prevMessageCount = 0;
shouldScroll = true;
isFirstTimeFetchHistory = true;
keyboardDidShowListener.remove();
};
}, []);
function getImages() {
const imageMessages = messages.filter(
(message) => message.type === 'image',
);
const images = imageMessages.map((message) => {
const url = message.content;
const parseResult = /width=(\d+)&height=(\d+)/.exec(url);
return {
url: `${url.startsWith('//') ? 'https:' : ''}${url}`,
...(parseResult
? {
width: +parseResult[1],
height: +parseResult[2],
}
: {}),
};
});
return images;
}
function scrollToEnd(time = 0) {
if (time > 200) {
return;
}
if ($scrollView.current) {
$scrollView.current!.scrollToEnd({ animated: false });
}
setTimeout(() => {
scrollToEnd(time + 50);
}, 50);
}
function handleKeyboardShow() {
scrollToEnd();
}
async function handleRefresh() {
if (refreshing) {
return;
}
if (isFirstTimeFetchHistory && isAndroid) {
isFirstTimeFetchHistory = false;
return;
}
setRefreshing(true);
let err = null;
let result = null;
if (isLogin) {
[err, result] = await fetch('getLinkmanHistoryMessages', {
linkmanId: focus,
existCount: messages.length,
});
} else {
[err, result] = await fetch('getDefalutGroupHistoryMessages', {
existCount: messages.length,
});
}
if (!err) {
if (result.length > 0) {
action.addLinkmanHistoryMessages(focus, result);
} else {
Toast.warning('没有更多消息了');
}
}
setTimeout(() => {
setRefreshing(false);
}, 1000);
}
/**
* 加载历史消息后, 自动滚动到合适位置
*/
function handleContentSizeChange(
contentWidth: number,
contentHeight: number,
) {
if (prevContentHeight === 0) {
$scrollView.current!.scrollTo({
x: 0,
y: 0,
animated: false,
});
} else if (
contentHeight !== prevContentHeight &&
messages.length - prevMessageCount > 1
) {
$scrollView.current!.scrollTo({
x: 0,
y: contentHeight - prevContentHeight - 60,
animated: false,
});
}
prevContentHeight = contentHeight;
prevMessageCount = messages.length;
}
function handleScroll(event: any) {
const {
layoutMeasurement,
contentSize,
contentOffset,
} = event.nativeEvent;
shouldScroll =
contentOffset.y >
contentSize.height - layoutMeasurement.height * 1.2;
if (contentOffset.y < (isiOS ? 0 : 50)) {
handleRefresh();
}
}
function openImageViewer(url: string) {
const images = getImages();
const index = images.findIndex(
(image) => image.url.indexOf(url) !== -1,
);
toggleShowImageViewerDialog(true);
setImageViewerIndex(index);
}
function renderMessage(message: MessageType) {
return (
<Message
key={message._id}
message={message}
isSelf={self === message.from._id}
shouldScroll={shouldScroll}
scrollToEnd={scrollToEnd}
openImageViewer={openImageViewer}
/>
);
}
function closeImageViewerDialog() {
toggleShowImageViewerDialog(false);
}
return (
<ScrollView
style={styles.container}
ref={$scrollView}
onContentSizeChange={handleContentSizeChange}
scrollEventThrottle={50}
onScroll={handleScroll}
>
{messages.map((message) => renderMessage(message))}
<Modal
visible={showImageViewerDialog}
transparent
onRequestClose={closeImageViewerDialog}
>
<ImageViewer
imageUrls={getImages()}
index={imageViewerIndex}
onClick={closeImageViewerDialog}
onSwipeDown={closeImageViewerDialog}
saveToLocalByLongPress={false}
renderImage={(image) => (
<Image
source={{
uri: image.source.uri,
cache: 'force-cache',
headers: {
Referer: referer,
},
}}
style={image.style}
/>
)}
/>
</Modal>
</ScrollView>
);
}
export default MessageList;
const styles = StyleSheet.create({
container: {
paddingTop: 8,
paddingBottom: 8,
},
});
================================================
FILE: packages/app/src/pages/Chat/SystemMessage.tsx
================================================
import { View, Text } from 'native-base';
import React from 'react';
import { StyleSheet } from 'react-native';
import { Message } from '../../types/redux';
import { getPerRandomColor } from '../../utils/getRandomColor';
type Props = {
message: Message;
};
function SystemMessage({ message }: Props) {
const { content, from } = message;
return (
<View style={styles.container}>
<Text
style={[
styles.text,
{ color: getPerRandomColor(from.originUsername as string) },
]}
>
{from.originUsername}
</Text>
<Text style={styles.text}>{content}</Text>
</View>
);
}
export default SystemMessage;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
},
text: {
fontSize: 14,
},
});
================================================
FILE: packages/app/src/pages/Chat/TextMessage.tsx
================================================
import { View, Text } from 'native-base';
import React from 'react';
import { TouchableOpacity, Linking, StyleSheet } from 'react-native';
import Expression from '../../components/Expression';
import { Message } from '../../types/redux';
import expressions from '../../utils/expressions';
type Props = {
message: Message;
isSelf: boolean;
};
function TextMessage({ message, isSelf }: Props) {
const children = [];
let copy = message.content;
function push(str: string) {
children.push(
<Text
key={Math.random()}
style={{ color: isSelf ? 'white' : '#444' }}
>
{str}
</Text>,
);
}
// 处理文本消息中的表情和链接
let offset = 0;
while (copy.length > 0) {
const regex = /#\(([\u4e00-\u9fa5a-z]+)\)|https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
const matchResult = regex.exec(copy);
if (matchResult) {
const r = matchResult[0];
const e = matchResult[1];
const i = copy.indexOf(r);
if (r[0] === '#') {
// 表情消息
const index = expressions.default.indexOf(e);
if (index !== -1) {
// 处理从开头到匹配位置的文本
if (i > 0) {
push(copy.substring(0, i));
}
children.push(
<Expression
key={Math.random()}
style={styles.expression}
size={30}
index={index}
/>,
);
offset += i + r.length;
}
} else {
// 链接消息
if (i > 0) {
push(copy.substring(0, i));
}
children.push(
<TouchableOpacity
key={Math.random()}
onPress={() => Linking.openURL(r)}
>
{// Do not nest in view error in dev environment
process.env.NODE_ENV === 'development' ? (
<View>
<Text style={{ color: '#001be5' }}>{r}</Text>
</View>
) : (
<Text style={{ color: '#001be5' }}>{r}</Text>
)}
</TouchableOpacity>,
);
offset += i + r.length;
}
copy = copy.substr(i + r.length);
} else {
break;
}
}
// 处理剩余文本
if (offset < message.content.length) {
push(message.content.substring(offset, message.content.length));
}
return <View style={[styles.container]}>{children}</View>;
}
export default TextMessage;
const styles = StyleSheet.create({
container: {
// width: '100%',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-end',
},
text: {
flexShrink: 1,
},
textSelf: {
color: 'white',
},
expression: {
marginLeft: 1,
marginRight: 1,
transform: [
{
translateY: 3,
},
],
},
});
================================================
FILE: packages/app/src/pages/ChatList/ChatList.tsx
================================================
import React, { useState } from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import { Header, Item, Icon, Input } from 'native-base';
import { Actions } from 'react-native-router-flux';
import Linkman from './Linkman';
import { useLinkmans } from '../../hooks/useStore';
import { Linkman as LinkmanType } from '../../types/redux';
import PageContainer from '../../components/PageContainer';
import { search } from '../../service';
import { isiOS } from '../../utils/platform';
export default function ChatList() {
const [searchKeywords, updateSearchKeywords] = useState('');
const linkmans = useLinkmans();
async function handleSearch() {
const result = await search(searchKeywords);
updateSearchKeywords('');
Actions.push('searchResult', result);
}
function renderLinkman(linkman: LinkmanType) {
const { _id: linkmanId, unread, messages, createTime } = linkman;
const lastMessage =
messages.length > 0 ? messages[messages.length - 1] : null;
let time = new Date(createTime);
let preview = '暂无消息';
if (lastMessage) {
time = new Date(lastMessage.createTime);
preview =
lastMessage.type === 'text'
? `${lastMessage.content}`
: `[${lastMessage.type}]`;
if (linkman.type === 'group') {
preview = `${lastMessage.from.username}: ${preview}`;
}
}
return (
<Linkman
key={linkmanId}
id={linkmanId}
name={linkman.name}
avatar={linkman.avatar}
preview={preview}
time={time}
unread={unread}
linkman={linkman}
lastMessageId={lastMessage ? lastMessage._id : ''}
/>
);
}
return (
<PageContainer>
<Header searchBar rounded noShadow style={styles.searchContainer}>
<Item style={styles.searchItem}>
<Icon name="ios-search" style={styles.searchIcon} />
<Input
style={styles.searchText}
placeholder="搜索群组/用户"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="search"
value={searchKeywords}
onChangeText={updateSearchKeywords}
onSubmitEditing={handleSearch}
/>
</Item>
</Header>
<ScrollView style={styles.messageList}>
{linkmans && linkmans.map((linkman) => renderLinkman(linkman))}
</ScrollView>
</PageContainer>
);
}
const styles = StyleSheet.create({
messageList: {},
searchContainer: {
marginTop: isiOS ? 0 : 5,
backgroundColor: 'transparent',
height: 42,
borderBottomWidth: 0,
},
searchItem: {
backgroundColor: 'rgba(255,255,255,0.5)',
},
searchIcon: {
color: '#555',
},
searchText: {
fontSize: 14,
},
});
================================================
FILE: packages/app/src/pages/ChatList/ChatListRightButton.tsx
================================================
import { View, Icon } from 'native-base';
import React, { useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
import Dialog from 'react-native-dialog';
import { createGroup } from '../../service';
import action from '../../state/action';
function ChatListRightButton() {
const [showDialog, toggleDialog] = useState(false);
const [groupName, updateGroupName] = useState('');
function handleCloseDialog() {
updateGroupName('');
toggleDialog(false);
}
async function handleCreateGroup() {
const group = await createGroup(groupName);
if (group) {
action.addLinkman({
...group,
type: 'group',
unread: 1,
messages: [],
});
action.setFocus(group._id);
handleCloseDialog();
Actions.push('chat', { title: group.name });
}
}
return (
<>
<TouchableOpacity onPress={() => toggleDialog(true)}>
<View style={styles.container}>
<Icon name="add-outline" style={styles.icon} />
</View>
</TouchableOpacity>
<Dialog.Container visible={showDialog}>
<Dialog.Title>创建群组</Dialog.Title>
<Dialog.Description>请输入群组名</Dialog.Description>
<Dialog.Input
value={groupName}
onChangeText={updateGroupName}
autoCapitalize="none"
autoFocus
autoCorrect={false}
/>
<Dialog.Button label="取消" onPress={handleCloseDialog} />
<Dialog.Button label="创建" onPress={handleCreateGroup} />
</Dialog.Container>
</>
);
}
export default ChatListRightButton;
const styles = StyleSheet.create({
container: {
width: 44,
height: 44,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
icon: {
color: 'white',
fontSize: 32,
},
});
================================================
FILE: packages/app/src/pages/ChatList/Linkman.tsx
================================================
import React from 'react';
import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
import Time from '../../utils/time';
import action from '../../state/action';
import Avatar from '../../components/Avatar';
import { Linkman as LinkmanType } from '../../types/redux';
import { formatLinkmanName } from '../../utils/linkman';
import fetch from '../../utils/fetch';
type Props = {
id: string;
name: string;
avatar: string;
preview: string;
time: Date;
unread: number;
lastMessageId: string;
linkman: LinkmanType;
};
export default function Linkman({
id,
name,
avatar,
preview,
time,
unread,
lastMessageId,
linkman,
}: Props) {
function formatTime() {
const nowTime = new Date();
if (Time.isToday(nowTime, time)) {
return Time.getHourMinute(time);
}
if (Time.isYesterday(nowTime, time)) {
return '昨天';
}
if (Time.isSameYear(nowTime, time)) {
return Time.getMonthDate(time);
}
return Time.getYearMonthDate(time);
}
function handlePress() {
action.setFocus(id);
Actions.chat({ title: formatLinkmanName(linkman) });
if (id && lastMessageId) {
fetch('updateHistory', { linkmanId: id, messageId: lastMessageId });
}
}
return (
<TouchableOpacity onPress={handlePress}>
<View style={styles.container}>
<Avatar src={avatar} size={50} />
<View style={styles.content}>
<View style={styles.nickTime}>
<Text style={styles.nick}>{name}</Text>
<Text style={styles.time}>{formatTime()}</Text>
</View>
<View style={styles.previewUnread}>
<Text style={styles.preview} numberOfLines={1}>
{preview}
</Text>
{unread > 0 ? (
<View style={styles.unread}>
<Text style={styles.unreadText}>
{unread > 99 ? '99' : unread}
</Text>
</View>
) : null}
</View>
</View>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
height: 70,
alignItems: 'center',
paddingLeft: 16,
paddingRight: 16,
},
content: {
flex: 1,
marginLeft: 8,
},
nickTime: {
flexDirection: 'row',
justifyContent: 'space-between',
},
nick: {
fontSize: 16,
color: '#333',
},
time: {
fontSize: 14,
color: '#888',
},
previewUnread: {
marginTop: 8,
flexDirection: 'row',
justifyContent: 'space-between',
},
preview: {
flex: 1,
fontSize: 14,
color: '#666',
},
unread: {
backgroundColor: '#2a7bf6',
width: 18,
height: 18,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5,
},
unreadText: {
fontSize: 10,
color: 'white',
},
});
================================================
FILE: packages/app/src/pages/ChatList/SelfInfo.tsx
================================================
import { Text, View } from 'native-base';
import React from 'react';
import { StyleSheet } from 'react-native';
import Avatar from '../../components/Avatar';
import { useIsLogin, useStore, useTheme, useUser } from '../../hooks/useStore';
function SelfInfo() {
const isLogin = useIsLogin();
const user = useUser();
const { primaryTextColor10 } = useTheme();
const { connect } = useStore();
if (!isLogin) {
return null;
}
const { avatar, username } = user;
return (
<View style={[styles.container]}>
<View>
<Avatar src={avatar} size={32} />
<View
style={[
styles.onlineStatus,
connect ? styles.online : styles.offline,
]}
/>
</View>
<View>
<Text style={[styles.nickname, { color: primaryTextColor10 }]}>
{username}
</Text>
</View>
</View>
);
}
export default SelfInfo;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
height: 38,
paddingLeft: 8,
paddingRight: 8,
},
avatar: {
position: 'relative',
},
nickname: {
marginLeft: 8,
},
onlineStatus: {
width: 10,
height: 10,
borderRadius: 5,
position: 'absolute',
right: 0,
bottom: 0,
},
online: {
backgroundColor: 'rgba(94, 212, 92, 1)',
},
offline: {
backgroundColor: 'rgba(206, 12, 35, 1)',
},
});
================================================
FILE: packages/app/src/pages/GroupInfo/GroupInfo.tsx
================================================
import React from 'react';
import { Button, Text, View } from 'native-base';
import { StyleSheet } from 'react-native';
import { Actions } from 'react-native-router-flux';
import PageContainer from '../../components/PageContainer';
import Avatar from '../../components/Avatar';
import { useFocusLinkman, useLinkmans } from '../../hooks/useStore';
import { Linkman } from '../../types/redux';
import action from '../../state/action';
import { getLinkmanHistoryMessages, joinGroup } from '../../service';
type Props = {
group: {
_id: string;
avatar: string;
name: string;
members: number;
};
};
function GroupInfo({ group }: Props) {
const { _id, avatar, name, members } = group;
const linkmans = useLinkmans();
const linkman = linkmans.find(
(x) => x._id === _id && x.type === 'group',
) as Linkman;
const isJoined = !!linkman;
const currentLinkman = useFocusLinkman() as Linkman;
function handleSendMessage() {
action.setFocus(group._id);
if (currentLinkman._id === group._id) {
Actions.popTo('chat');
} else {
Actions.popTo('_chatlist');
Actions.push('chat', { title: group.name });
}
}
async function handleJoinGroup() {
const newLinkman = await joinGroup(_id);
if (newLinkman) {
action.addLinkman({
...newLinkman,
type: 'group',
unread: 0,
messages: [],
});
const messages = await getLinkmanHistoryMessages(_id, 0);
action.addLinkmanHistoryMessages(_id, messages);
action.setFocus(_id);
Actions.popTo('_chatlist');
Actions.push('chat', { title: newLinkman.name });
}
}
return (
<PageContainer>
<View style={styles.container}>
<View style={styles.userContainer}>
<Avatar src={avatar} size={88} />
<Text style={styles.nick}>{name}</Text>
</View>
<View style={styles.infoContainer}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>人数:</Text>
<Text style={styles.infoValue}>{members}</Text>
</View>
</View>
<View style={styles.buttonContainer}>
{isJoined ? (
<Button
primary
block
style={styles.button}
onPress={handleSendMessage}
>
<Text>发送消息</Text>
</Button>
) : (
<Button
primary
block
style={styles.button}
onPress={handleJoinGroup}
>
<Text>加入群组</Text>
</Button>
)}
</View>
</View>
</PageContainer>
);
}
export default GroupInfo;
const styles = StyleSheet.create({
container: {
paddingTop: 20,
paddingLeft: 16,
paddingRight: 16,
},
userContainer: {
alignItems: 'center',
},
infoContainer: {
marginTop: 20,
},
infoRow: {
flexDirection: 'row',
},
infoLabel: {
color: '#666',
},
infoValue: {
color: '#333',
marginLeft: 12,
},
nick: {
color: '#333',
marginTop: 6,
},
buttonContainer: {
marginTop: 20,
},
button: {
marginBottom: 12,
},
});
================================================
FILE: packages/app/src/pages/GroupProfile/GroupProfile.tsx
================================================
import { View, Text, Button } from 'native-base';
import React from 'react';
import { Alert, Pressable, ScrollView, StyleSheet } from 'react-native';
import { Actions } from 'react-native-router-flux';
import Avatar from '../../components/Avatar';
import PageContainer from '../../components/PageContainer';
import { useFocusLinkman, useSelfId } from '../../hooks/useStore';
import { deleteGroup, leaveGroup } from '../../service';
import action from '../../state/action';
import { Group } from '../../types/redux';
function GroupProfile() {
const linkman = useFocusLinkman() as Group;
const self = useSelfId();
const isGroupCreator = linkman.creator === self;
function getOS(os: string) {
return os === 'Windows Server 2008 R2 / 7' ? 'Windows 7' : os;
}
function ShowEnvironment(environment: string) {
Alert.alert('设备信息', environment);
}
async function handleLeaveGroup() {
if (isGroupCreator) {
const isSuccess = await deleteGroup(linkman._id);
if (isSuccess) {
action.removeLinkman(linkman._id);
Actions.popTo('_chatlist', { title: '' });
}
} else {
const isSuccess = await leaveGroup(linkman._id);
if (isSuccess) {
action.removeLinkman(linkman._id);
Actions.popTo('_chatlist', { title: '' });
}
}
}
return (
<PageContainer>
<ScrollView style={styles.container}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>功能</Text>
<Button danger onPress={handleLeaveGroup}>
<Text>{isGroupCreator ? '解散群组' : '退出群组'}</Text>
</Button>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>在线成员</Text>
{linkman.members.map((member) => (
<View key={member._id} style={styles.member}>
<Avatar src={member.user.avatar} size={24} />
<Text style={styles.memberName}>
{member.user.username}
</Text>
<Pressable
style={styles.memberInfoContainer}
onPress={() =>
ShowEnvironment(member.environment)
}
>
<Text style={styles.memberInfo}>
{member.browser} {getOS(member.os)}
</Text>
</Pressable>
</View>
))}
</View>
</ScrollView>
</PageContainer>
);
}
export default GroupProfile;
const styles = StyleSheet.create({
container: {
paddingLeft: 12,
paddingRight: 12,
paddingTop: 8,
paddingBottom: 8,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 12,
},
member: {
flexDirection: 'row',
alignItems: 'center',
height: 32,
},
memberName: {
fontSize: 14,
color: '#333',
marginLeft: 8,
},
memberInfoContainer: {
flex: 1,
},
memberInfo: {
fontSize: 12,
color: '#666',
textAlign: 'right',
},
});
================================================
FILE: packages/app/src/pages/LoginSignup/Base.tsx
================================================
import React, { useRef, useState } from 'react';
import { Alert, StyleSheet, Text, TextInput } from 'react-native';
import { Form, Label, Button, View } from 'native-base';
import { Actions } from 'react-native-router-flux';
import PageContainer from '../../components/PageContainer';
type Props = {
buttonText: string;
jumpText: string;
jumpPage: string;
onSubmit: (username: string, password: string) => void;
};
export default function Base({
buttonText,
jumpText,
jumpPage,
onSubmit,
}: Props) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const $username = useRef<TextInput>();
const $password = useRef<TextInput>();
function handlePress() {
$username.current!.blur();
$password.current!.blur();
onSubmit(username, password);
}
function handleJump() {
if (Actions[jumpPage]) {
Actions.replace(jumpPage);
} else {
Alert.alert(`跳转 ${jumpPage} 失败`);
}
}
return (
<PageContainer>
<View style={styles.container}>
<Form>
<Label style={styles.label}>用户名</Label>
<TextInput
style={[styles.input]}
// @ts-ignore
ref={$username}
clearButtonMode="while-editing"
onChangeText={setUsername}
autoCapitalize="none"
autoCompleteType="username"
/>
<Label style={styles.label}>密码</Label>
<TextInput
style={[styles.input]}
// @ts-ignore
ref={$password}
secureTextEntry
clearButtonMode="while-editing"
onChangeText={setPassword}
autoCapitalize="none"
autoCompleteType="password"
/>
</Form>
<Button
primary
block
style={styles.button}
onPress={handlePress}
>
<Text style={styles.buttonText}>{buttonText}</Text>
</Button>
<Button transparent style={styles.signup} onPress={handleJump}>
<Text style={styles.signupText}>{jumpText}</Text>
</Button>
</View>
</PageContainer>
);
}
const styles = StyleSheet.create({
container: {
paddingLeft: 12,
paddingRight: 12,
paddingTop: 20,
},
button: {
marginTop: 18,
},
buttonText: {
fontSize: 18,
color: '#fafafa',
},
signup: {
alignSelf: 'flex-end',
},
signupText: {
color: '#2a7bf6',
fontSize: 14,
},
label: {
marginBottom: 8,
},
input: {
height: 42,
fontSize: 16,
borderRadius: 6,
marginBottom: 12,
paddingLeft: 6,
borderWidth: 1,
borderColor: '#777',
},
});
================================================
FILE: packages/app/src/pages/LoginSignup/Login.tsx
================================================
import React from 'react';
import { Container } from 'native-base';
import { Actions } from 'react-native-router-flux';
import fetch from '../../utils/fetch';
import platform from '../../utils/platform';
import action from '../../state/action';
import Base from './Base';
import { setStorageValue } from '../../utils/storage';
import { Friend, Group } from '../../types/redux';
export default function Login() {
async function handleSubmit(username: string, password: string) {
const [err, res] = await fetch('login', {
username,
password,
...platform,
});
if (!err) {
const user = res;
action.setUser(user);
const linkmanIds = [
...user.groups.map((g: Group) => g._id),
...user.friends.map((f: Friend) => f._id),
];
const [err2, linkmans] = await fetch('getLinkmansLastMessagesV2', {
linkmans: linkmanIds,
});
if (!err2) {
action.setLinkmansLastMessages(linkmans);
}
Actions.pop();
await setStorageValue('token', res.token);
}
}
return (
<Container>
<Base
buttonText="登录"
jumpText="注册新用户"
jumpPage="signup"
onSubmit={handleSubmit}
/>
</Container>
);
}
================================================
FILE: packages/app/src/pages/LoginSignup/Signup.tsx
================================================
import React from 'react';
import { Container, Toast } from 'native-base';
import { Actions } from 'react-native-router-flux';
import fetch from '../../utils/fetch';
import platform from '../../utils/platform';
import action from '../../state/action';
import Base from './Base';
import { setStorageValue } from '../../utils/storage';
import { Friend, Group } from '../../types/redux';
export default function Signup() {
async function handleSubmit(username: string, password: string) {
const [err, res] = await fetch('register', {
username,
password,
...platform,
});
if (!err) {
Toast.show({
text: '创建成功',
type: 'success',
});
const user = res;
action.setUser(user);
const linkmanIds = [
...user.groups.map((g: Group) => g._id),
...user.friends.map((f: Friend) => f._id),
];
const [err2, linkmans] = await fetch('getLinkmansLastMessagesV2', {
linkmans: linkmanIds,
});
if (!err2) {
action.setLinkmansLastMessages(linkmans);
}
Actions.chatlist();
await setStorageValue('token', res.token);
}
}
return (
<Container>
<Base
buttonText="注册"
jumpText="已有账号? 去登陆"
jumpPage="login"
onSubmit={handleSubmit}
/>
</Container>
);
}
================================================
FILE: packages/app/src/pages/Other/Other.tsx
================================================
import {
Body,
Button,
Content,
Icon,
List,
ListItem,
Right,
Text,
Toast,
View,
} from 'native-base';
import React, { useEffect, useState } from 'react';
import { Linking, StyleSheet } from 'react-native';
import { Actions } from 'react-native-router-flux';
import PageContainer from '../../components/PageContainer';
import { useIsLogin } from '../../hooks/useStore';
import socket from '../../socket';
import action from '../../state/action';
import { getStorageValue, removeStorageValue } from '../../utils/storage';
import appInfo from '../../../app.json';
import Avatar from '../../components/Avatar';
import PrivacyPolicy, { PrivacyPolicyStorageKey } from './PrivacyPolicy';
function getIsNight() {
const hour = new Date().getHours();
return hour >= 18 || hour < 6;
}
function Other() {
const isLogin = useIsLogin();
const [isNight, setIsNight] = useState(getIsNight());
const [showPrivacyPolicy, togglePrivacyPolicy] = useState(false);
async function getPrivacyPolicyStatus() {
const privacyPoliceStorageValue = await getStorageValue(
PrivacyPolicyStorageKey,
);
togglePrivacyPolicy(privacyPoliceStorageValue !== 'true');
}
useEffect(() => {
const timer = setInterval(() => {
setIsNight(getIsNight());
}, 1000);
getPrivacyPolicyStatus();
return () => {
clearInterval(timer);
};
}, []);
async function logout() {
action.logout();
await removeStorageValue('token');
Toast.show({ text: '您已经退出登录' });
socket.disconnect();
socket.connect();
}
async function login() {
const privacyPoliceStorageValue = await getStorageValue(
PrivacyPolicyStorageKey,
);
if (privacyPoliceStorageValue !== 'true') {
togglePrivacyPolicy(true);
return;
}
Actions.push('login');
}
return (
<PageContainer>
<Content>
<View style={styles.app}>
<Avatar
src={
isNight
? require('../../../icon.png')
: require('../../assets/images/wuzeiniang.gif')
}
size={100}
/>
<Text style={styles.name}>
fiora v{appInfo.expo.version}
</Text>
</View>
<List style={styles.list}>
<ListItem
icon
onPress={() =>
Linking.openURL(
'https://github.com/yinxin630/fiora-app',
)
}
>
<Body>
<Text style={styles.listItemTitle}>源码</Text>
</Body>
<Right>
<Icon
active
name="arrow-forward"
style={styles.listItemArrow}
/>
</Right>
</ListItem>
<ListItem
icon
onPress={() =>
Linking.openURL('https://www.suisuijiang.com')
}
>
<Body>
<Text style={styles.listItemTitle}>作者</Text>
</Body>
<Right>
<Icon
active
name="arrow-forward"
style={styles.listItemArrow}
/>
</Right>
</ListItem>
<ListItem
icon
onPress={() =>
Linking.openURL('https://fiora.suisuijiang.com')
}
>
<Body>
<Text style={styles.listItemTitle}>
fiora 网页版
</Text>
</Body>
<Right>
<Icon
active
name="arrow-forward"
style={styles.listItemArrow}
/>
</Right>
</ListItem>
</List>
</Content>
{isLogin ? (
<Button
danger
block
style={styles.logoutButton}
onPress={logout}
>
<Text>退出登录</Text>
</Button>
) : (
<Button block style={styles.logoutButton} onPress={login}>
<Text>登录 / 注册</Text>
</Button>
)}
<View style={styles.copyrightContainer}>
<Text style={styles.copyright}>
Copyright© 2015-
{new Date().getFullYear()} 碎碎酱
</Text>
</View>
<PrivacyPolicy
visible={showPrivacyPolicy}
onClose={() => togglePrivacyPolicy(false)}
/>
</PageContainer>
);
}
const styles = StyleSheet.create({
logoutButton: {
marginLeft: 12,
marginRight: 12,
},
app: {
alignItems: 'center',
paddingTop: 12,
},
name: {
marginTop: 6,
color: '#222',
},
list: {
marginTop: 20,
backgroundColor: 'rgba(255, 255, 255, 0.4)',
},
listItemTitle: {
color: '#333',
},
listItemArrow: {
color: '#999',
},
github: {
fontSize: 26,
color: '#000',
},
copyrightContainer: {
marginTop: 12,
marginBottom: 6,
},
copyright: {
fontSize: 10,
textAlign: 'center',
color: '#666',
},
});
export default Other;
================================================
FILE: packages/app/src/pages/Other/PrivacyPolicy.tsx
================================================
import { Text } from 'native-base';
import React from 'react';
import { Linking, StyleSheet, TouchableOpacity } from 'react-native';
import Dialog from 'react-native-dialog';
import { removeStorageValue, setStorageValue } from '../../utils/storage';
export const PrivacyPolicyStorageKey = 'privacy-policy';
type Props = {
visible: boolean;
onClose: () => void;
};
function PrivacyPolicy({ visible, onClose }: Props) {
function handleClickPrivacyPolicy() {
Linking.openURL('https://fiora.suisuijiang.com/PrivacyPolicy.html');
}
async function handleAgree() {
await setStorageValue(PrivacyPolicyStorageKey, 'true');
onClose();
}
async function handleDisagree() {
await removeStorageValue(PrivacyPolicyStorageKey);
onClose();
}
return (
<Dialog.Container visible={visible}>
<Dialog.Title>服务协议和隐私条款</Dialog.Title>
<Dialog.Description style={styles.container}>
欢迎使用 fiora
APP。我们非常重视您的个人信息和隐私保护,在您使用之前,请务必审慎阅读
<TouchableOpacity onPress={handleClickPrivacyPolicy}>
<Text style={styles.text}>《隐私政策》</Text>
</TouchableOpacity>
,并充分理解协议条款内容。我们将严格按照您同意的各项条款使用您的个人信息,以便为您提供更好的服务。
</Dialog.Description>
<Dialog.Button label="不同意" onPress={handleDisagree} />
<Dialog.Button label="同意" onPress={handleAgree} />
</Dialog.Container>
);
}
export default PrivacyPolicy;
const styles = StyleSheet.create({
container: {
textAlign: 'left',
},
text: {
fontSize: 12,
color: '#2a7bf6',
},
});
================================================
FILE: packages/app/src/pages/Other/Sponsor.tsx
================================================
import { View, Text } from 'native-base';
import React from 'react';
import { StyleSheet } from 'react-native';
import Dialog from 'react-native-dialog';
type Props = {
visible: boolean;
onClose: () => void;
onOK: () => void;
};
function Sponsor({ visible, onClose, onOK }: Props) {
return (
<Dialog.Container visible={visible}>
<Dialog.Title>赞助</Dialog.Title>
<Dialog.Description>
<View>
<Text style={styles.text}>
如果你觉得这个聊天室还不错的话, 希望能赞助一下~~
</Text>
<Text style={styles.tip}>
请在转账备注中填写您的 fiora 账号
</Text>
</View>
</Dialog.Description>
<Dialog.Button label="关闭" onPress={onClose} />
<Dialog.Button label="赞助" onPress={onOK} />
</Dialog.Container>
);
}
export default Sponsor;
const styles = StyleSheet.create({
text: {
fontSize: 14,
color: '#333',
marginTop: 16,
},
tip: {
fontSize: 12,
color: '#666',
textAlign: 'center',
marginTop: 12,
},
});
================================================
FILE: packages/app/src/pages/SearchResult/SearchResult.tsx
================================================
import React from 'react';
import { Tab, Tabs, Text, View } from 'native-base';
import { ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
import { Actions } from 'react-native-router-flux';
import PageContainer from '../../components/PageContainer';
import Avatar from '../../components/Avatar';
type Props = {
groups: {
_id: string;
name: string;
avatar: string;
members: number;
}[];
users: {
_id: string;
username: string;
avatar: string;
}[];
};
function SearchResult({ groups, users }: Props) {
function handleClickGroup(group: any) {
Actions.push('groupInfo', { group });
}
function handleClickUser(user: any) {
Actions.push('userInfo', { user });
}
return (
<PageContainer disableSafeAreaView>
<Tabs
style={styles.container}
tabContainerStyle={{ backgroundColor: 'transparent' }}
>
<Tab
heading={`群组(${groups.length})`}
tabStyle={{ backgroundColor: 'transparent' }}
activeTabStyle={{ backgroundColor: 'transparent' }}
>
<PageContainer>
<ScrollView>
{groups.map((group) => (
<TouchableOpacity
key={group._id}
onPress={() => handleClickGroup(group)}
>
<View style={styles.item}>
<Avatar src={group.avatar} size={40} />
<View style={styles.groupInfo}>
<Text style={styles.groupName}>
{group.name}
</Text>
<Text style={styles.groupMembers}>
{group.members}人
</Text>
</View>
</View>
</TouchableOpacity>
))}
</ScrollView>
</PageContainer>
</Tab>
<Tab
heading={`用户(${users.length})`}
tabStyle={{ backgroundColor: 'transparent' }}
activeTabStyle={{ backgroundColor: 'transparent' }}
>
<PageContainer>
<ScrollView>
{users.map((user) => (
<TouchableOpacity
key={user._id}
onPress={() => handleClickUser(user)}
>
<View style={styles.item}>
<Avatar src={user.avatar} size={40} />
<Text style={styles.username}>
{user.username}
</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
</PageContainer>
</Tab>
</Tabs>
</PageContainer>
);
}
export default SearchResult;
const styles = StyleSheet.create({
container: {
backgroundColor: 'transparent',
},
item: {
height: 56,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 16,
paddingRight: 16,
},
groupInfo: {
marginLeft: 8,
},
groupName: {
color: '#444',
},
groupMembers: {
fontSize: 14,
color: '#888',
marginTop: 1,
},
username: {
color: '#444',
marginLeft: 8,
},
});
================================================
FILE: packages/app/src/pages/UserInfo/UserInfo.tsx
================================================
import React from 'react';
import { Button, Text, View } from 'native-base';
import { StyleSheet } from 'react-native';
import { Actions } from 'react-native-router-flux';
import PageContainer from '../../components/PageContainer';
import Avatar from '../../components/Avatar';
import {
useFocusLinkman,
useIsAdmin,
useLinkmans,
useSelfId,
} from '../../hooks/useStore';
import { Linkman } from '../../types/redux';
import action from '../../state/action';
import {
addFriend,
deleteFriend,
getLinkmanHistoryMessages,
sealUser,
sealUserOnlineIp,
} from '../../service';
import getFriendId from '../../utils/getFriendId';
import Toast from '../../components/Toast';
type Props = {
user: {
_id: string;
avatar: string;
tag: string;
username: string;
};
};
function UserInfo({ user }: Props) {
const { _id, avatar, username } = user;
const linkmans = useLinkmans();
const friend = linkmans.find((linkman) =>
linkman._id.includes(_id),
) as Linkman;
const isFriend = friend && friend.type === 'friend';
const isAdmin = useIsAdmin();
const currentLinkman = useFocusLinkman() as Linkman;
const self = useSelfId();
function handleSendMessage() {
action.setFocus(friend._id);
if (currentLinkman._id === friend._id) {
Actions.pop();
} else {
Actions.popTo('_chatlist');
Actions.push('chat', { title: friend.name });
}
}
async function handleDeleteFriend() {
const isSuccess = await deleteFriend(_id);
if (isSuccess) {
action.removeLinkman(friend._id);
if (currentLinkman._id === friend._id) {
Actions.popTo('_chatlist');
} else {
Actions.pop();
}
}
}
async function handleAddFriend() {
const newLinkman = await addFriend(_id);
const friendId = getFriendId(_id, self);
if (newLinkman) {
if (friend) {
action.updateFriendProperty(friend._id, 'type', 'friend');
const messages = await getLinkmanHistoryMessages(
friend._id,
friend.messages.length,
);
action.addLinkmanHistoryMessages(friend._id, messages);
} else {
action.addLinkman({
...newLinkman,
_id: friendId,
name: username,
type: 'friend',
unread: 0,
messages: [],
from: self,
to: {
_id,
avatar,
username,
},
});
const messages = await getLinkmanHistoryMessages(friendId, 0);
action.addLinkmanHistoryMessages(friendId, messages);
}
action.setFocus(friendId);
if (currentLinkman._id === friend?._id) {
Actions.pop();
} else {
Actions.popTo('_chatlist');
Actions.push('chat', { title: newLinkman.username });
}
}
}
async function handleSealUser() {
const isSuccess = await sealUser(username);
if (isSuccess) {
Toast.success('封禁用户成功');
}
}
async function handleSealIp() {
const isSuccess = await sealUserOnlineIp(_id);
if (isSuccess) {
Toast.success('封禁用户当前ip成功');
}
}
return (
<PageContainer>
<View style={styles.container}>
<View style={styles.userContainer}>
<Avatar src={avatar} size={88} />
<Text style={styles.nick}>{username}</Text>
</View>
<View style={styles.buttonContainer}>
{isFriend ? (
<>
<Button
primary
block
style={styles.button}
onPress={handleSendMessage}
>
<Text>发送消息</Text>
</Button>
<Button
primary
block
danger
style={styles.button}
onPress={handleDeleteFriend}
>
<Text>删除好友</Text>
</Button>
</>
) : (
<Button
primary
block
style={styles.button}
onPress={handleAddFriend}
>
<Text>加为好友</Text>
</Button>
)}
{isAdmin && (
<>
<Button
primary
block
danger
style={styles.button}
onPress={handleSealUser}
>
<Text>封禁用户</Text>
</Button>
<Button
primary
block
danger
style={styles.button}
onPress={handleSealIp}
>
<Text>封禁 ip</Text>
</Button>
</>
)}
</View>
</View>
</PageContainer>
);
}
export default UserInfo;
const styles = StyleSheet.create({
container: {
paddingTop: 20,
paddingLeft: 16,
paddingRight: 16,
},
userContainer: {
alignItems: 'center',
},
nick: {
color: '#333',
marginTop: 6,
},
buttonContainer: {
marginTop: 20,
},
button: {
marginBottom: 12,
},
});
================================================
FILE: packages/app/src/service.ts
================================================
import { User } from './types/redux';
import fetch from './utils/fetch';
function saveUsername(username: string) {
window.localStorage.setItem('username', username);
}
/**
* 注册新用户
* @param username 用户名
* @param password 密码
* @param os 系统
* @param browser 浏览器
* @param environment 环境信息
*/
export async function register(
username: string,
password: string,
os = '',
browser = '',
environment = '',
) {
const [err, user] = await fetch('register', {
username,
password,
os,
browser,
environment,
});
if (err) {
return null;
}
saveUsername(user.username);
return user;
}
/**
* 使用账密登录
* @param username 用户名
* @param password 密码
* @param os 系统
* @param browser 浏览器
* @param environment 环境信息
*/
export async function login(
username: string,
password: string,
os = '',
browser = '',
environment = '',
) {
const [err, user] = await fetch('login', {
username,
password,
os,
browser,
environment,
});
if (err) {
return null;
}
saveUsername(user.username);
return user;
}
/**
* 使用token登录
* @param token 登录token
* @param os 系统
* @param browser 浏览器
* @param environment 环境信息
*/
export async function loginByToken(
token: string,
os = '',
browser = '',
environment = '',
) {
const [err, user] = await fetch(
'loginByToken',
{
token,
os,
browser,
environment,
},
{ toast: false },
);
if (err) {
return null;
}
saveUsername(user.username);
return user;
}
/**
* 游客模式登陆
* @param os 系统
* @param browser 浏览器
* @param environment 环境信息
*/
export async function guest(os = '', browser = '', environment = '') {
const [err, res] = await fetch('guest', { os, browser, environment });
if (err) {
return null;
}
return res;
}
/**
* 修用户头像
* @param avatar 新头像链接
*/
export async function changeAvatar(avatar: string) {
const [error] = await fetch('changeAvatar', { avatar });
return !error;
}
/**
* 修改用户密码
* @param oldPassword 旧密码
* @param newPassword 新密码
*/
export async function changePassword(oldPassword: string, newPassword: string) {
const [error] = await fetch('changePassword', {
oldPassword,
newPassword,
});
return !error;
}
/**
* 修改用户名
* @param username 新用户名
*/
export async function changeUsername(username: string) {
const [error] = await fetch('changeUsername', {
username,
});
return !error;
}
/**
* 修改群组名
* @param groupId 目标群组
* @param name 新名字
*/
export async function changeGroupName(groupId: string, name: string) {
const [error] = await fetch('changeGroupName', { groupId, name });
return !error;
}
/**
* 修改群头像
* @param groupId 目标群组
* @param name 新头像
*/
export async function changeGroupAvatar(groupId: string, avatar: string) {
const [error] = await fetch('changeGroupAvatar', { groupId, avatar });
return !error;
}
/**
* 创建群组
* @param name 群组名
*/
export async function createGroup(name: string) {
const [, group] = await fetch('createGroup', { name });
return group;
}
/**
* 删除群组
* @param groupId 群组id
*/
export async function deleteGroup(groupId: string) {
const [error] = await fetch('deleteGroup', { groupId });
return !error;
}
/**
* 加入群组
* @param groupId 群组id
*/
export async function joinGroup(groupId: string) {
const [, group] = await fetch('joinGroup', { groupId });
return group;
}
/**
* 离开群组
* @param groupId 群组id
*/
export async function leaveGroup(groupId: string) {
const [error] = await fetch('leaveGroup', { groupId });
return !error;
}
/**
* 添加好友
* @param userId 目标用户id
*/
export async function addFriend(userId: string) {
const [, user] = await fetch<User>('addFriend', { userId });
return user;
}
/**
* 删除好友
* @param userId 目标用户id
*/
export async function deleteFriend(userId: string) {
const [err] = await fetch('deleteFriend', { userId });
return !err;
}
/**
* 获取联系人历史消息
* @param linkmanId 联系人id
* @param existCount 客户端已有消息条数
*/
export async function getLinkmanHistoryMessages(
linkmanId: string,
existCount: number,
) {
const [, messages] = await fetch('getLinkmanHistoryMessages', {
linkmanId,
existCount,
});
return messages;
}
/**
* 获取默认群组的历史消息
* @param existCount 客户端已有消息条数
*/
export async function getDefaultGroupHistoryMessages(existCount: number) {
const [, messages] = await fetch('getDefaultGroupHistoryMessages', {
existCount,
});
return messages;
}
/**
* 搜索用户和群组
* @param keywords 关键字
*/
export async function search(keywords: string) {
const [, result] = await fetch('search', { keywords });
return result;
}
/**
* 搜索表情包
* @param keywords 关键字
*/
export async function searchExpression(keywords: string) {
const [, result] = await fetch('searchExpression', { keywords });
return result;
}
/**
* 发送消息
* @param to 目标
* @param type 消息类型
* @param content 消息内容
*/
export async function sendMessage(to: string, type: string, content: string) {
return fetch('sendMessage', { to, type, content });
}
/**
* 删除消息
* @param messageId 要删除的消息id
*/
export async function deleteMessage(messageId: string) {
const [err] = await fetch('deleteMessage', { messageId });
return !err;
}
/**
* 获取目标群组的在线用户列表
* @param groupId 目标群id
*/
export async function getGroupOnlineMembers(groupId: string) {
const [, members] = await fetch('getGroupOnlineMembers', { groupId });
return members;
}
/**
* 获取默认群组的在线用户列表
*/
export async function getDefaultGroupOnlineMembers() {
const [, members] = await fetch('getDefaultGroupOnlineMembers');
return members;
}
/**
* 封禁用户
* @param username 目标用户名
*/
export async function sealUser(username: string) {
const [err] = await fetch('sealUser', { username });
return !err;
}
/**
* 封禁ip
* @param ip ip地址
*/
export async function sealIp(ip: string) {
const [err] = await fetch('sealIp', { ip });
return !err;
}
/**
* 封禁用户所有在线ip
* @param userId 用户id
*/
export async function sealUserOnlineIp(userId: string) {
const [err] = await fetch('sealUserOnlineIp', { userId });
return !err;
}
/**
* 获取封禁用户列表
*/
export async function getSealList() {
const [, sealList] = await fetch('getSealList');
return sealList;
}
/**
* 重置指定用户的密码
* @param username 目标用户名
*/
export async function resetUserPassword(username: string) {
const [, res] = await fetch('resetUserPassword', { username });
return res;
}
/**
* 更新指定用户的标签
* @param username 目标用户名
* @param tag 标签
*/
export async function setUserTag(username: string, tag: string) {
const [err] = await fetch('setUserTag', { username, tag });
return !err;
}
/**
* 获取在线用户 ip
* @param userId 用户id
*/
export async function getUserIps(userId: string) {
const [, res] = await fetch('getUserIps', { userId });
return res;
}
export async function getUserOnlineStatus(userId: string) {
const [, res] = await fetch('getUserOnlineStatus', { userId });
return res && res.isOnline;
}
export async function setNotificationToken(token: string) {
const [, res] = await fetch(
'setNotificationToken',
{ token },
{ toast: false },
);
return res && res.isOK;
}
================================================
FILE: packages/app/src/socket.ts
================================================
import IO from 'socket.io-client';
import Toast from './components/Toast';
import action from './state/action';
import store from './state/store';
import {
AddLinkmanAction,
AddLinkmanActionType,
AddLinkmanHistoryMessagesAction,
AddLinkmanHistoryMessagesActionType,
AddlinkmanMessageAction,
AddlinkmanMessageActionType,
ConnectAction,
ConnectActionType,
DeleteLinkmanMessageAction,
DeleteLinkmanMessageActionType,
Friend,
Group,
Message,
RemoveLinkmanAction,
RemoveLinkmanActionType,
SetGuestAction,
SetGuestActionType,
State,
Temporary,
UpdateGroupPropertyAction,
UpdateGroupPropertyActionType,
UpdateUserPropertyAction,
UpdateUserPropertyActionType,
User,
} from './types/redux';
import getFriendId from './utils/getFriendId';
import platform from './utils/platform';
import { getStorageValue } from './utils/storage';
const { dispatch } = store;
const options = {
transports: ['websocket'],
};
const host = 'http://10.132.67.127:9200';
const socket = IO(host, options);
function fetch<T = any>(
event: string,
data: any = {},
{ toast = true } = {},
): Promise<[string | null, T | null]> {
return new Promise((resolve) => {
socket.emit(event, data, (res: any) => {
if (typeof res === 'string') {
if (toast) {
Toast.danger(res);
}
resolve([res, null]);
} else {
resolve([null, res]);
}
});
});
}
async function guest() {
const [err, res] = await fetch('guest', {});
if (!err) {
dispatch({
type: SetGuestActionType,
linkmans: [res],
} as SetGuestAction);
}
}
socket.on('connect', async () => {
dispatch({
type: ConnectActionType,
value: true,
} as ConnectAction);
const token = await getStorageValue('token');
if (token) {
const [err, res] = await fetch(
'loginByToken',
{
token,
...platform,
},
{ toast: false },
);
if (err) {
guest();
} else {
const user = res;
action.setUser(user);
const linkmanIds = [
...user.groups.map((g: Group) => g._id),
...user.friends.map((f: Friend) => f._id),
];
const [err2, linkmans] = await fetch('getLinkmansLastMessagesV2', {
linkmans: linkmanIds,
});
if (!err2) {
action.setLinkmansLastMessages(linkmans);
}
}
} else {
guest();
}
});
socket.on('disconnect', () => {
dispatch({
type: ConnectActionType,
value: false,
} as ConnectAction);
});
socket.on('message', (message: Message) => {
const state = store.getState() as State;
const linkman = state.linkmans.find((x) => x._id === message.to);
if (linkman) {
dispatch({
type: AddlinkmanMessageActionType,
linkmanId: message.to,
message,
} as AddlinkmanMessageAction);
} else {
const newLinkman: Temporary = {
_id: getFriendId((state.user as User)._id, message.from._id),
type: 'temporary',
createTime: Date.now(),
avatar: message.from.avatar,
name: message.from.username,
messages: [],
unread: 1,
};
dispatch({
type: AddLinkmanActionType,
linkman: newLinkman,
focus: false,
} as AddLinkmanAction);
fetch('getLinkmanHistoryMessages', {
linkmanId: newLinkman._id,
existCount: 0,
}).then(([err, res]) => {
if (!err) {
dispatch({
type: AddLinkmanHistoryMessagesActionType,
linkmanId: newLinkman._id,
messages: res,
} as AddLinkmanHistoryMessagesAction);
}
});
}
});
socket.on(
'changeGroupName',
({ groupId, name }: { groupId: string; name: string }) => {
dispatch({
type: UpdateGroupPropertyActionType,
groupId,
key: 'name',
value: name,
} as UpdateGroupPropertyAction);
},
);
socket.on('deleteGroup', ({ groupId }: { groupId: string }) => {
dispatch({
type: RemoveLinkmanActionType,
linkmanId: groupId,
} as RemoveLinkmanAction);
});
socket.on('changeTag', (tag: string) => {
dispatch({
type: UpdateUserPropertyActionType,
key: 'tag',
value: tag,
} as UpdateUserPropertyAction);
});
socket.on(
'deleteMessage',
({ linkmanId, messageId }: { linkmanId: string; messageId: string }) => {
dispatch({
type: DeleteLinkmanMessageActionType,
linkmanId,
messageId,
} as DeleteLinkmanMessageAction);
},
);
socket.connect();
export default socket;
================================================
FILE: packages/app/src/state/action.ts
================================================
import getFriendId from '../utils/getFriendId';
import store from './store';
import {
ConnectActionType,
ConnectAction,
Friend,
SetUserActionType,
SetUserAction,
SetLinkmanMessagesAction,
SetGuestActionType,
SetGuestAction,
LogoutActionType,
UpdateUserPropertyActionType,
UpdateUserPropertyAction,
AddlinkmanMessageActionType,
AddlinkmanMessageAction,
AddLinkmanHistoryMessagesActionType,
AddLinkmanHistoryMessagesAction,
UpdateSelfMessageActionType,
UpdateSelfMessageAction,
SetFocusAction,
AddLinkmanAction,
RemoveLinkmanActionType,
RemoveLinkmanAction,
SetFriendAction,
UpdateUIPropertyActionType,
UpdateUIPropertyAction,
Group,
Message,
Linkman,
UpdateGroupPropertyActionType,
UpdateGroupPropertyAction,
UpdateFriendPropertyActionType,
UpdateFriendPropertyAction,
DeleteLinkmanMessageAction,
DeleteLinkmanMessageActionType,
} from '../types/redux';
const { dispatch } = store;
function connect() {
dispatch({
type: ConnectActionType,
value: true,
} as ConnectAction);
}
function disconnect() {
dispatch({
type: ConnectActionType,
value: false,
} as ConnectAction);
}
function setUser(user: any) {
user.groups.forEach((group: Group) => {
Object.assign(group, {
type: 'group',
unread: 0,
messages: [],
members: [],
});
});
user.friends.forEach((friend: Friend) => {
Object.assign(friend, {
type: 'friend',
_id: getFriendId(friend.from, friend.to._id),
messages: [],
unread: 0,
avatar: friend.to.avatar,
name: friend.to.username,
to: friend.to._id,
});
});
const linkmans = [...user.groups, ...user.friends];
dispatch({
type: SetUserActionType,
user: {
...user,
groups: null,
friends: null,
},
linkmans,
} as SetUserAction);
}
function setLinkmansLastMessages(
linkmans: SetLinkmanMessagesAction['linkmans'],
) {
dispatch({
type: 'SetLinkmanMessages',
linkmans,
} as SetLinkmanMessagesAction);
}
function setGuest(defaultGroup: Group) {
dispatch({
type: SetGuestActionType,
linkmans: [
Object.assign(defaultGroup, {
type: 'group',
unread: 0,
members: [],
}),
],
} as SetGuestAction);
}
function logout() {
dispatch({
type: LogoutActionType,
});
}
function setAvatar(avatar: string) {
dispatch({
type: UpdateUserPropertyActionType,
key: 'avatar',
value: avatar,
} as UpdateUserPropertyAction);
}
function updateUserProperty(key: string, value: any) {
dispatch({
type: UpdateUserPropertyActionType,
key,
value,
} as UpdateUserPropertyAction);
}
function addLinkmanMessage(linkmanId: string, message: Message) {
dispatch({
type: AddlinkmanMessageActionType,
linkmanId,
message,
} as AddlinkmanMessageAction);
}
function deleteLinkmanMessage(linkmanId: string, messageId: string) {
dispatch({
type: DeleteLinkmanMessageActionType,
linkmanId,
messageId,
} as DeleteLinkmanMessageAction);
}
function addLinkmanHistoryMessages(linkmanId: string, messages: Message[]) {
dispatch({
type: AddLinkmanHistoryMessagesActionType,
linkmanId,
messages,
} as AddLinkmanHistoryMessagesAction);
}
function updateSelfMessage(
linkmanId: string,
messageId: string,
message: Message,
) {
dispatch({
type: UpdateSelfMessageActionType,
linkmanId,
messageId,
message,
} as UpdateSelfMessageAction);
}
function setFocus(linkmanId: string) {
dispatch({
type: 'SetFocus',
linkmanId,
} as SetFocusAction);
}
function setGroupMembers(groupId: string, members: Group['members']) {
dispatch({
type: UpdateGroupPropertyActionType,
groupId,
key: 'members',
value: members,
} as UpdateGroupPropertyAction);
}
function setGroupAvatar(groupId: string, avatar: string) {
dispatch({
type: UpdateGroupPropertyActionType,
groupId,
key: 'avatar',
value: avatar,
} as UpdateGroupPropertyAction);
}
function updateGroupProperty(groupId: string, key: string, value: any) {
dispatch({
type: UpdateGroupPropertyActionType,
groupId,
key,
value,
} as UpdateGroupPropertyAction);
}
function updateFriendProperty(userId: string, key: string, value: any) {
dispatch({
type: UpdateFriendPropertyActionType,
userId,
key,
value,
} as UpdateFriendPropertyAction);
}
function addLinkman(linkman: Linkman, focus = false) {
if (linkman.type === 'group') {
linkman.members = [];
linkman.messages = [];
linkman.unread = 0;
}
dispatch({
type: 'AddLinkman',
linkman,
focus,
} as AddLinkmanAction);
}
function removeLinkman(linkmanId: string) {
dispatch({
type: RemoveLinkmanActionType,
linkmanId,
} as RemoveLinkmanAction);
}
function setFriend(linkmanId: string, from: Friend['from'], to: Friend['to']) {
dispatch({
type: 'SetFriend',
linkmanId,
from,
to,
} as SetFriendAction);
}
function loading(text: string) {
dispatch({
type: UpdateUIPropertyActionType,
key: 'loading',
value: text,
} as UpdateUIPropertyAction);
}
export default {
setUser,
setGuest,
connect,
disconnect,
logout,
setAvatar,
updateUserProperty,
setLinkmansLastMessages,
setFocus,
setGroupMembers,
setGroupAvatar,
addLinkman,
removeLinkman,
setFriend,
updateGroupProperty,
updateFriendProperty,
addLinkmanMessage,
addLinkmanHistoryMessages,
updateSelfMessage,
deleteLinkmanMessage,
loading,
};
================================================
FILE: packages/app/src/state/reducer.ts
================================================
import produce from 'immer';
import deepmerge from 'deepmerge';
import {
State,
ActionTypes,
ConnectActionType,
LogoutActionType,
SetUserActionType,
SetGuestActionType,
UpdateUserPropertyActionType,
SetLinkmanMessagesActionType,
SetFocusActionType,
SetFriendActionType,
Friend,
AddLinkmanActionType,
RemoveLinkmanActionType,
AddlinkmanMessageActionType,
AddLinkmanHistoryMessagesActionType,
UpdateSelfMessageActionType,
UpdateUIPropertyActionType,
Group,
UpdateGroupPropertyActionType,
UpdateFriendPropertyActionType,
DeleteLinkmanMessageActionType,
User,
Linkman,
} from '../types/redux';
import convertMessage from '../utils/convertMessage';
export function mergeLinkmans(
linkmans1: Linkman[],
linkmans2: Linkman[],
): Linkman[] {
const linkmansMap2 = linkmans2.reduce(
(map: { [key: string]: Linkman }, linkman) => {
map[linkman._id] = linkman;
return map;
},
{},
);
const unionListingsIdSet = new Set(
linkmans1
.map((linkman) => linkman._id)
.filter((linkmanId) => !!linkmansMap2[linkmanId]),
);
const linkmans = [
...linkmans1.filter((linkman) => unionListingsIdSet.has(linkman._id)),
...linkmans2.filter((linkman) => !unionListingsIdSet.has(linkman._id)),
];
return linkmans.map((linkman) => {
if (unionListingsIdSet.has(linkman._id)) {
return deepmerge(linkman as any, linkmansMap2[linkman._id] as any, {
customMerge: (key) => {
if (key === 'messages') {
// The new linkman data at this time does not have messages
// So keep the old messages
return () => linkman.messages;
}
},
});
}
return linkman;
});
}
const initialState = {
linkmans: [],
focus: '',
connect: true,
ui: {
ready: false,
loading: '', // 全局loading文本内容, 为空则不展示
primaryColor: '5,159,149',
primaryTextColor: '255, 255, 255',
},
};
const reducer = produce((state: State = initialState, action: ActionTypes) => {
switch (action.type) {
case ConnectActionType: {
state.connect = action.value;
return state;
}
case LogoutActionType: {
return initialState;
}
case SetUserActionType: {
const currentUserId = (state.user as User)?._id;
if (!currentUserId || currentUserId !== action.user._id) {
// No user or guest user or different user
state.user = action.user;
state.linkmans = action.linkmans;
} else {
// Same user. Deep merge to reserve history messages;
// But these history messages must be overwritten in SetLinkmanMessagesAction
// Otherwise, there may be errors when fetch history messages later
state.user = action.user;
state.linkmans = mergeLinkmans(state.linkmans, action.linkmans);
}
return state;
}
case SetGuestActionType: {
action.linkmans.forEach((linkman) => {
linkman.messages.forEach(convertMessage);
});
state.linkmans = action.linkmans;
return state;
}
case UpdateUserPropertyActionType: {
// @ts-ignore
state!.user[action.key] = action.value;
return state;
}
case SetLinkmanMessagesActionType: {
state.linkmans = state.linkmans.map((linkman) => ({
...linkman,
...(action.linkmans[linkman._id]
? {
messages: action.linkmans[linkman._id].messages.map(convertMessage),
unread: action.linkmans[linkman._id].unread,
}
: {}),
})) as Linkman[];
state.linkmans.sort((linkman1, linkman2) => {
const lastMessageTime1 =
linkman1.messages.length > 0
? linkman1.messages[linkman1.messages.length - 1]
.createTime
: linkman1.createTime;
const lastMessageTime2 =
linkman2.messages.length > 0
? linkman2.messages[linkman2.messages.length - 1]
.createTime
: linkman2.createTime;
return new Date(lastMessageTime1) < new Date(lastMessageTime2)
? 1
: -1;
});
if (
!state.focus ||
!state.linkmans.find((linkman) => linkman._id === state.focus)
) {
state.focus =
state.linkmans.length > 0 ? state.linkmans[0]._id : '';
}
return state;
}
case UpdateGroupPropertyActionType: {
const group = state.linkmans.find(
(linkman) =>
linkman.type === 'group' && linkman._id === action.groupId,
) as Group;
if (group) {
// @ts-ignore
group[action.key] = action.value;
}
return state;
}
case UpdateFriendPropertyActionType: {
const friend = state.linkmans.find(
(linkman) =>
linkman.type !== 'group' && linkman._id === action.userId,
) as Friend;
if (friend) {
// @ts-ignore
friend[action.key] = action.value;
}
return state;
}
case SetFocusActionType: {
const targetLinkman = state.linkmans.find(
(linkman) => linkman._id === action.linkmanId,
);
if (targetLinkman) {
state.focus = action.linkmanId;
targetLinkman.unread = 0;
}
return state;
}
case SetFriendActionType: {
const friend = state.linkmans.find(
(linkman) => linkman._id === action.linkmanId,
) as Friend;
if (friend) {
friend.type = 'friend';
friend.from = action.from;
friend.to = action.to;
friend.unread = 0;
state.focus = action.linkmanId;
}
return state;
}
case AddLinkmanActionType: {
state.linkmans.unshift(action.linkman);
if (action.focus) {
state.focus = action.linkman._id;
}
return state;
}
case RemoveLinkmanActionType: {
const index = state.linkmans.findIndex(
(linkman) => linkman._id === action.linkmanId,
);
if (index !== -1) {
state.linkmans.splice(index, 1);
if (state.focus === action.linkmanId) {
state.focus =
state.linkmans.length > 0 ? state.linkmans[0]._id : '';
}
}
return state;
}
case AddlinkmanMessageActionType: {
const targetLinkman = state.linkmans.find(
(linkman) => linkman._id === action.linkmanId,
);
if (targetLinkman) {
if (state.focus !== targetLinkman._id) {
targetLinkman.unread += 1;
}
targetLinkman.messages.push(convertMessage(action.message));
if (targetLinkman.messages.length > 500) {
targetLinkman.messages.slice(250);
}
}
return state;
}
case AddLinkmanHistoryMessagesActionType: {
const targetLinkman = state.linkmans.find(
(linkman) => linkman._id === action.linkmanId,
);
if (targetLinkman) {
targetLinkman.messages.unshift(
...action.messages.map(convertMessage),
);
}
return state;
}
case UpdateSelfMessageActionType: {
const targetLinkman = state.linkmans.find(
(linkman) => linkman._id === action.linkmanId,
);
if (targetLinkman) {
const targetMessage = targetLinkman.messages.find(
(message) => message._id === action.messageId,
);
if (targetMessage) {
Object.assign(
targetMessage,
convertMessage(action.message),
);
}
}
return state;
}
case DeleteLinkmanMessageActionType: {
const targetLinkman = state.linkmans.find(
(linkman) => linkman._id === action.linkmanId,
);
if (targetLinkman) {
const targetMessage = targetLinkman.messages.find(
(message) => message._id === action.messageId,
);
if (targetMessage) {
targetMessage.deleted = true;
convertMessage(targetMessage);
}
}
return state;
}
case UpdateUIPropertyActionType: {
state.ui[action.key] = action.value;
return state;
}
default: {
return state;
}
}
}, initialState);
export default reducer;
================================================
FILE: packages/app/src/state/store.ts
================================================
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(
// @ts-ignore
reducer,
// @ts-ignore
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
);
export default store;
================================================
FILE: packages/app/src/types/global.d.ts
================================================
declare module '@react-native-toolkit/triangle';
declare module 'react-native-dialog';
declare module '*.png';
================================================
FILE: packages/app/src/types/redux.ts
================================================
export const ConnectActionType = 'SetConnect';
export type ConnectAction = {
type: typeof ConnectActionType;
value: boolean;
};
export const SetUserActionType = 'SetUser';
export type SetUserAction = {
type: typeof SetUserActionType;
user: User;
linkmans: Linkman[];
};
export const SetLinkmanMessagesActionType = 'SetLinkmanMessages';
export type SetLinkmanMessagesAction = {
type: typeof SetLinkmanMessagesActionType;
linkmans: Record<
string,
{
messages: Message[];
unread: number;
}
>;
};
export const SetGuestActionType = 'SetGuest';
export type SetGuestAction = {
type: typeof SetGuestActionType;
linkmans: Linkman[];
};
export const LogoutActionType = 'Logout';
export type LogoutAction = {
type: typeof LogoutActionType;
};
export const UpdateUserPropertyActionType = 'UpdateUserProperty';
export type UpdateUserPropertyAction = {
type: typeof UpdateUserPropertyActionType;
key: keyof User;
value: any;
};
export const AddlinkmanMessageActionType = 'AddlinkmanMessage';
export type AddlinkmanMessageAction = {
type: typeof AddlinkmanMessageActionType;
linkmanId: string;
message: Message;
};
export const DeleteLinkmanMessageActionType = 'DeleteLinkmanMessage';
export type DeleteLinkmanMessageAction = {
type: typeof DeleteLinkmanMessageActionType;
linkmanId: string;
messageId: string;
};
export const AddLinkmanHistoryMessagesActionType = 'AddLinkmanHistoryMessages';
export type AddLinkmanHistoryMessagesAction = {
type: typeof AddLinkmanHistoryMessagesActionType;
linkmanId: string;
messages: Message[];
};
export const UpdateSelfMessageActionType = 'UpdateSelfMessageActionType';
export type UpdateSelfMessageAction = {
type: typeof UpdateSelfMessageActionType;
linkmanId: string;
messageId: string;
message: Message;
};
export const SetFocusActionType = 'SetFocus';
export type SetFocusAction = {
type: typeof SetFocusActionType;
linkmanId: string;
};
export const UpdateGroupPropertyActionType = 'UpdateGroupProperty';
export type UpdateGroupPropertyAction = {
type: typeof UpdateGroupPropertyActionType;
groupId: string;
key: keyof Group;
value: any;
};
export const UpdateFriendPropertyActionType = 'UpdateFriendProperty';
export type UpdateFriendPropertyAction = {
type: typeof UpdateFriendPropertyActionType;
userId: string;
key: keyof Group;
value: any;
};
export const AddLinkmanActionType = 'AddLinkman';
export type AddLinkmanAction = {
type: typeof AddLinkmanActionType;
linkman: Linkman;
focus: boolean;
};
export const RemoveLinkmanActionType = 'RemoveLinkmanActionType';
export type RemoveLinkmanAction = {
type: typeof RemoveLinkmanActionType;
linkmanId: string;
};
export const SetFriendActionType = 'SetFriend';
export type SetFriendAction = {
type: typeof SetFriendActionType;
linkmanId: string;
from: Friend['from'];
to: Friend['to'];
};
export const UpdateUIPropertyActionType = 'UpdateUIPropertyActionType';
export type UpdateUIPropertyAction = {
type: typeof UpdateUIPropertyActionType;
key: keyof State['ui'];
value: any;
};
export type ActionTypes =
| ConnectAction
| SetUserAction
| SetLinkmanMessagesAction
| UpdateGroupPropertyAction
| SetGuestAction
| SetFocusAction
| SetFriendAction
| AddLinkmanAction
| RemoveLinkmanAction
| AddlinkmanMessageAction
| AddLinkmanHistoryMessagesAction
| DeleteLinkmanMessageAction
| UpdateSelfMessageAction
| UpdateUserPropertyAction
| UpdateUIPropertyAction
| UpdateFriendPropertyAction
| LogoutAction;
export type Message = {
_id: string;
type: string;
content: string;
createTime: number;
percent?: number;
loading?: boolean;
from: {
_id: string;
username: string;
avatar: string;
tag: string;
originUsername?: string;
};
to: string;
deleted?: boolean;
};
export type Group = {
_id: string;
type: 'group';
name: string;
avatar: string;
messages: Message[];
unread: number;
members: {
_id: string;
user: {
_id: string;
username: string;
avatar: string;
};
os: string;
browser: string;
environment: string;
}[];
creator: string;
createTime: number;
};
export type Friend = {
_id: string;
type: 'friend';
name: string;
avatar: string;
from: string;
to: {
_id: string;
avatar: string;
username: string;
};
messages: Message[];
unread: number;
createTime: number;
isOnline?: boolean;
};
export type Temporary = {
_id: string;
type: 'temporary';
name: string;
avatar: string;
messages: Message[];
unread: number;
createTime: number;
isOnline?: boolean;
};
export type Linkman = Group | Friend | Temporary;
export type User = {
_id: string;
username: string;
avatar: string;
tag: string;
isAdmin: boolean;
notificationTokens: string[];
createTime: number;
};
export type State = {
user?: User;
linkmans: Linkman[];
focus: string;
connect: boolean;
ui: {
loading: string;
primaryColor: string;
primaryTextColor: string;
};
};
================================================
FILE: packages/app/src/types/socket.ts
================================================
export type Socket = {
on: (event: string, callback: (...params: any) => void) => void;
};
================================================
FILE: packages/app/src/utils/constant.ts
================================================
export const referer = 'https://fiora.suisuijiang.com/';
================================================
FILE: packages/app/src/utils/convertMessage.ts
================================================
// function convertRobot10Message(message) {
// if (message.from._id === '5adad39555703565e7903f79') {
// try {
// const parseMessage = JSON.parse(message.content);
// message.from.tag = parseMessage.source;
// message.from.avatar = parseMessage.avatar;
// message.from.username = parseMessage.username;
// message.type = parseMessage.type;
// message.content = parseMessage.content;
// } catch (err) {
// console.warn('解析robot10消息失败', err);
// }
// }
// }
import { Message } from '../types/redux';
const WuZeiNiangImage = require('../assets/images/wuzeiniang.gif');
function convertSystemMessage(message: Message) {
if (message.type === 'system') {
message.from._id = 'system';
message.from.originUsername = message.from.username;
message.from.username = '乌贼娘殿下';
message.from.avatar = WuZeiNiangImage;
message.from.tag = 'system';
let content = null;
try {
content = JSON.parse(message.content);
} catch {
content = {
command: 'parse-error',
};
}
switch (content?.command) {
case 'roll': {
message.content = `掷出了${content.value}点 (上限${content.top}点)`;
break;
}
case 'rps': {
message.content = `使出了 ${content.value}`;
break;
}
default: {
message.content = '不支持的指令';
}
}
} else if (message.deleted) {
message.type = 'system';
message.from._id = 'system';
message.from.originUsername = message.from.username;
message.from.username = '乌贼娘殿下';
message.from.avatar = WuZeiNiangImage;
message.from.tag = 'system';
message.content = `撤回了消息`;
}
return message;
}
/**
* 处理文本消息的html转义字符
* @param {Object} message 消息
*/
function convertMessageHtml(message: Message) {
if (message.type === 'text') {
message.content = message.content
.replace(/</g, '<')
.replace(/>/g, '>');
}
return message;
}
export default function convertMessage(message: Message) {
convertSystemMessage(message);
convertMessageHtml(message);
return message;
}
================================================
FILE: packages/app/src/utils/expressions.ts
================================================
export default {
default: [
'呵呵',
'哈哈',
'吐舌',
'啊',
'酷',
'怒',
'开心',
'汗',
'泪',
'黑线',
'鄙视',
'不高兴',
'真棒',
'钱',
'疑问',
'阴险',
'吐',
'咦',
'委屈',
'花心',
'呼',
'笑眼',
'冷',
'太开心',
'滑稽',
'勉强',
'狂汗',
'乖',
'睡觉',
'惊哭',
'升起',
'惊讶',
'喷',
'爱心',
'心碎',
'玫瑰',
'礼物',
'星星月亮',
'太阳',
'音乐',
'灯泡',
'蛋糕',
'彩虹',
'钱币',
'咖啡',
'haha',
'胜利',
'大拇指',
'弱',
'ok',
],
};
================================================
FILE: packages/app/src/utils/fetch.ts
================================================
import Toast from '../components/Toast';
import socket from '../socket';
export default function fetch<T = any>(
event: string,
data: any = {},
{ toast = true } = {},
): Promise<[string | null, T | null]> {
return new Promise((resolve) => {
socket.emit(event, data, (res: any) => {
if (typeof res === 'string') {
if (toast) {
Toast.danger(res);
}
resolve([res, null]);
} else {
resolve([null, res]);
}
});
});
}
================================================
FILE: packages/app/src/utils/getFriendId.ts
================================================
export default function getFriendId(userId1: string, userId2: string) {
if (userId1 < userId2) {
return userId1 + userId2;
}
return userId2 + userId1;
}
================================================
FILE: packages/app/src/utils/getRandomColor.ts
================================================
import randomColor from 'randomcolor';
type ColorMode = 'dark' | 'bright' | 'light' | 'random';
/**
* 获取随机颜色, 刷新页面不变
* @param seed when passed will cause randomColor to return the same color each time
*/
export function getRandomColor(seed: string, luminosity: ColorMode = 'dark') {
return randomColor({
luminosity,
seed,
});
}
type Cache = {
[key: string]: string;
};
const cache: Cache = {};
/**
* 获取随机颜色, 刷新页面后重新随机
* @param seed 随机种子
* @param luminosity 亮度
*/
export function getPerRandomColor(
seed: string,
luminosity: ColorMode = 'dark',
) {
if (cache[seed]) {
return cache[seed];
}
cache[seed] = randomColor({ luminosity });
return cache[seed];
}
================================================
FILE: packages/app/src/utils/linkman.ts
================================================
import { Friend, Group, Linkman } from '../types/redux';
export function formatLinkmanName(linkman: Linkman) {
if (linkman!.type === 'group' && (linkman as Group).members.length > 0) {
return `${(linkman as Group).name} (${
(linkman as Group).members.length
})`;
}
if (
linkman!.type !== 'group' &&
(linkman as Friend).isOnline !== undefined
) {
return `${(linkman as Friend).name} (${
(linkman as Friend).isOnline ? '在线' : '离线'
})`;
}
return linkman.name;
}
================================================
FILE: packages/app/src/utils/platform.ts
================================================
import { Platform } from 'react-native';
import Constants from 'expo-constants';
// eslint-disable-next-line import/extensions
import packageInfo from '../../app.json';
const os = Platform.OS === 'ios' ? 'iOS' : 'Android';
export const isiOS = Platform.OS === 'ios';
export const isAndroid = Platform.OS === 'android';
export default {
os,
browser: 'App',
environment: `App ${
process.env.NODE_ENV === 'development'
? '开发版'
: packageInfo.expo.version
} on ${os} ${
isiOS ? Constants.platform?.ios?.systemVersion : Constants.systemVersion
} ${isiOS ? Constants.platform?.ios?.model : ''}`,
};
================================================
FILE: packages/app/src/utils/storage.ts
================================================
import AsyncStorage from '@react-native-async-storage/async-storage';
export async function getStorageValue(key: string) {
return AsyncStorage.getItem(key);
}
export async function setStorageValue(key: string, value: string) {
return AsyncStorage.setItem(key, value);
}
export async function removeStorageValue(key: string) {
return AsyncStorage.removeItem(key);
}
================================================
FILE: packages/app/src/utils/time.ts
================================================
export default {
isToday(time1: Date, time2: Date) {
return (
time1.getFullYear() === time2.getFullYear() &&
time1.getMonth() === time2.getMonth() &&
time1.getDate() === time2.getDate()
);
},
isYesterday(time1: Date, time2: Date) {
const prevDate = new Date(time1);
prevDate.setDate(time1.getDate() - 1);
return (
prevDate.getFullYear() === time2.getFullYear() &&
prevDate.getMonth() === time2.getMonth() &&
prevDate.getDate() === time2.getDate()
);
},
isSameYear(time1: Date, time2: Date) {
return time1.getFullYear() === time2.getFullYear();
},
getHourMinute(time: Date) {
const hours = time.getHours();
const minutes = time.getMinutes();
return `${hours < 10 ? `0${hours}` : hours}:${
minutes < 10 ? `0${minutes}` : minutes
}`;
},
getMonthDate(time: Date) {
return `${time.getMonth() + 1}/${time.getDate()}`;
},
getYearMonthDate(time: Date) {
return `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}`;
},
};
================================================
FILE: packages/app/src/utils/uploadFile.ts
================================================
import fetch from './fetch';
/**
* 上传文件
* @param blob 文件blob数据
* @param fileName 文件名
*/
export default async function uploadFile(
blob: Blob | string,
fileName: string,
isBase64 = false,
): Promise<string> {
const [uploadErr, result] = await fetch('uploadFile', {
file: blob,
fileName,
isBase64,
});
if (uploadErr) {
throw Error(`上传图片失败::${uploadErr}`);
}
return result.url;
}
export function getOSSFileUrl(url: string | number = '', process = '') {
if (typeof url === 'number') {
return url;
}
const [rawUrl = '', extraPrams = ''] = url.split('?');
if (/^\/\/cdn\.suisuijiang\.com/.test(rawUrl)) {
return `https:${rawUrl}?x-oss-process=${process}${
extraPrams ? `&${extraPrams}` : ''
}`;
}
if (url.startsWith('//')) {
return `https:${url}`;
}
if (url.startsWith('/')) {
return `https://fiora.suisuijiang.com${url}`;
}
return `${url}`;
}
================================================
FILE: packages/app/tests/state/reducer.test.ts
================================================
import { mergeLinkmans } from '../../src/state/reducer';
import { Linkman } from '../../src/types/redux';
describe('mergeLinkmans', () => {
it('should return linkmans which is newly and reserve history messages', () => {
const linkmans1 = [
{
_id: 'l1',
name: 'l1',
messages: [],
},
{
_id: 'l2',
name: 'l2',
messages: [
{
_id: 'm1',
},
{
_id: 'm2',
},
],
},
];
const linkmans2 = [
{
_id: 'l2',
name: 'l2',
messages: [
{
_id: 'm1',
},
{
_id: 'm2',
},
],
},
{
_id: 'l3',
name: 'l3',
messages: [],
},
];
const linkmans = mergeLinkmans(
linkmans1 as any,
linkmans2 as any,
) as Linkman[];
expect(linkmans).toHaveLength(2);
expect(linkmans[0]._id).toBe('l2');
expect(linkmans[1]._id).toBe('l3');
expect(linkmans[0].messages).toHaveLength(2);
});
});
================================================
FILE: packages/app/tsconfig.json
================================================
{
"extends": "../../tsconfig",
}
================================================
FILE: packages/assets/package.json
================================================
{
"name": "@fiora/assets",
"version": "1.0.0",
"license": "MIT",
"private": true
}
================================================
FILE: packages/bin/index.ts
================================================
import chalk from 'chalk';
import path from 'path';
import fs from 'fs';
const script = process.argv[2];
if (!script) {
console.log(chalk.green('没有任何事发生~'));
process.exit(0);
}
const file = path.resolve(__dirname, `scripts/${script}.ts`);
if (!fs.existsSync(file)) {
console.log(chalk.red(`[${script}] 脚本不存在`));
}
// @ts-ignore
import(file).then((module) => {
module.default();
});
================================================
FILE: packages/bin/package.json
================================================
{
"name": "@fiora/bin",
"version": "1.0.0",
"license": "MIT",
"private": true,
"scripts": {
"script": "ts-node --transpile-only index.ts"
},
"dependencies": {
"@fiora/config": "^1.0.0",
"@fiora/database": "^1.0.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
"detect-port": "^1.3.0",
"inquirer": "^8.1.2"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/detect-port": "^1.3.1",
"@types/inquirer": "^7.3.3"
}
}
================================================
FILE: packages/bin/scripts/deleteMessages.ts
================================================
import path from 'path';
import fs from 'fs';
import inquirer from 'inquirer';
import { promisify } from 'util';
import chalk from 'chalk';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
import Message from '@fiora/database/mongoose/models/message';
import History from '@fiora/database/mongoose/models/history';
export async function deleteMessages() {
const shouldDeleteAllMessages = await inquirer.prompt({
type: 'confirm',
name: 'result',
message: 'Confirm to delete all messages?',
});
if (!shouldDeleteAllMessages.result) {
return;
}
await initMongoDB();
const deleteResult = await Message.deleteMany({});
console.log('Delete result:', deleteResult);
const deleteHistoryResult = await History.deleteMany({});
console.log('Delete history result:', deleteHistoryResult);
const shouldDeleteAllFiles = await inquirer.prompt({
type: 'confirm',
name: 'result',
message: 'Confirm to delete all message files(Except OSS files)?',
});
if (!shouldDeleteAllFiles.result) {
return;
}
const files = await promisify(fs.readdir)(
path.resolve(__dirname, '../../server/public/'),
);
const iamgesAndFiles = files.filter(
(filename) =>
filename.startsWith('ImageMessage_') ||
filename.startsWith('FileMessage_'),
);
const unlinkAsync = promisify(fs.unlink);
await Promise.all(
iamgesAndFiles.map((file) =>
unlinkAsync(path.resolve(__dirname, '../../server/public/', file)),
),
);
console.log('Delete files:', chalk.green(iamgesAndFiles.length.toString()));
console.log(chalk.red(iamgesAndFiles.join('\n')));
console.log(chalk.green('Successfully deleted all messages'));
}
async function run() {
await deleteMessages();
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/deleteTodayRegisteredUsers.ts
================================================
/**
* Delete users created today and their related data
*/
import chalk from 'chalk';
import inquirer from 'inquirer';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
import User from '@fiora/database/mongoose/models/user';
import { deleteUser } from './deleteUser';
export async function deleteTodayRegisteredUsers() {
await initMongoDB();
const now = new Date();
const time = new Date(
`${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} 00:00:00`,
);
const users = await User.find({
createTime: {
$gte: time,
},
});
console.log(
`There are ${chalk.green(
users.length.toString(),
)} newly registered users today`,
);
if (users.length === 0) {
return;
}
const shouldDeleteUsers = await inquirer.prompt({
type: 'confirm',
name: 'result',
message: 'Confirm to delete these users?',
});
if (!shouldDeleteUsers.result) {
return;
}
await Promise.all(
users.map((user) => deleteUser(user._id.toString(), false)),
);
console.log(
chalk.green('Successfully deleted today’s newly registered users'),
);
}
async function run() {
await deleteTodayRegisteredUsers();
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/deleteUser.ts
================================================
/* eslint-disable no-console */
import chalk from 'chalk';
import inquirer from 'inquirer';
import User from '@fiora/database/mongoose/models/user';
import Message from '@fiora/database/mongoose/models/message';
import Group, { GroupDocument } from '@fiora/database/mongoose/models/group';
import Friend from '@fiora/database/mongoose/models/friend';
import History from '@fiora/database/mongoose/models/history';
import initMongoDB, { mongoose } from '@fiora/database/mongoose/initMongoDB';
export async function deleteUser(userIdOrName: string, confirm = true) {
if (!userIdOrName) {
console.log(chalk.red('Wrong command, [userIdOrName] is missing.'));
return;
}
await initMongoDB();
try {
const user = await User.findOne(
mongoose.isValidObjectId(userIdOrName)
? { _id: userIdOrName }
: { username: userIdOrName },
);
if (user) {
console.log(
'Found user:',
chalk.blue(user._id.toString()),
chalk.green(user.username),
);
if (confirm) {
const shouldDeleteUser = await inquirer.prompt({
type: 'confirm',
name: 'result',
message: 'Confirm to delete user?',
});
if (!shouldDeleteUser.result) {
return;
}
}
const messages = await Message.find({ from: user._id });
const deleteHistoryResult = await History.deleteMany({
message: {
$in: messages.map((message) => message.id),
},
});
console.log('Delete history result:', deleteHistoryResult);
console.log(chalk.yellow('Delete messages created by this user'));
const deleteMessageResult = await Message.deleteMany({
from: user._id,
});
console.log('Delete result:', deleteMessageResult);
console.log(
chalk.yellow('Leave the group that the user has joined'),
);
const groups = await Group.find({
members: user._id,
});
// eslint-disable-next-line no-inner-declarations
async function leaveGroup(group: GroupDocument) {
if (!user) {
return;
}
console.log('Leave', group.name);
const index = group.members.indexOf(user?._id);
group.members.splice(index, 1);
if (group.creator?.toString() === user?._id.toString()) {
// @ts-ignore
group.creator = null;
}
await group.save();
}
await Promise.all(groups.map(leaveGroup));
console.log(
chalk.yellow(
'Delete the friend relationship related to this user',
),
);
const deleteFriendResult1 = await Friend.deleteMany({
from: user._id,
});
const deleteFriendResult2 = await Friend.deleteMany({
to: user._id,
});
console.log(
'Delete result:',
deleteFriendResult1,
deleteFriendResult2,
);
console.log(chalk.yellow('Delete this user'));
const deleteUserResult = await User.deleteMany({
_id: user._id,
});
console.log('Delete result:', deleteUserResult);
console.log(chalk.green('Successfully deleted user'));
} else {
console.log(chalk.red(`User [${userIdOrName}] does not exist`));
}
} catch (err) {
console.log(chalk.red('Failed to delete user!', err.message));
}
}
async function run() {
const userIdOrName = process.argv[3];
await deleteUser(userIdOrName);
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/doctor.ts
================================================
import chalk from 'chalk';
import cp from 'child_process';
import fs from 'fs';
import path from 'path';
import detect from 'detect-port';
import server from '@fiora/config/server';
import initRedis from '@fiora/database/redis/initRedis';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
export async function doctor() {
console.log(chalk.yellow('===== Run Fiora Doctor ====='));
const nodeVersion = cp.execSync('node --version').toString();
console.log(
chalk.green(`node ${nodeVersion.slice(0, nodeVersion.length - 1)}`),
);
await initMongoDB();
console.log(chalk.green('MongoDB is OK'));
await (async () =>
new Promise((resolve) => {
const redis = initRedis();
redis.on('connect', resolve);
}))();
console.log(chalk.green('Redis is OK'));
const avaliablePort = await detect(server.port);
if (avaliablePort === server.port) {
console.log(chalk.green(`Port [${server.port}] is OK`));
} else {
console.log(chalk.red(`Port [${server.port}] was occupied`));
}
const indexFilePath = path.resolve(
__dirname,
'../../server/public/index.html',
);
const indexFile = fs.readFileSync(indexFilePath);
if (!indexFile) {
console.log(chalk.red('Homepage not exists'));
} else if (indexFile.toString().includes('默认首页')) {
console.log(
chalk.red(
'Homepage is default. Please build web client by [yarn build:web]',
),
);
} else {
console.log(chalk.green(`Homepage is OK`));
}
}
async function run() {
await doctor();
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/fixUsersAvatar.ts
================================================
import chalk from 'chalk';
import inquirer from 'inquirer';
import User from '@fiora/database/mongoose/models/user';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
export async function fixUsersAvatar(
searchValue: string,
replaceValue: string,
) {
searchValue = searchValue || 'fioraavatar';
replaceValue = replaceValue || 'fiora/avatar';
await initMongoDB();
const users = await User.find({ avatar: { $regex: 'fioraavatar' } });
if (users.length) {
console.log(chalk.red('Oh No!'), "Some user's avatar is wrong");
users.forEach((user) => {
console.log(user._id, user.username, user.avatar);
});
const shouldFix = await inquirer.prompt({
type: 'confirm',
name: 'result',
message: 'Confirm to fix?',
});
if (shouldFix.result) {
await Promise.all(
users.map((user) => {
user.avatar = user.avatar.replace(
searchValue,
replaceValue,
);
return user.save();
}),
);
console.log(chalk.green('Congratulations! Fixed!'));
}
} else {
console.log(chalk.green('OK!'), "All user's avatar is corrent");
}
}
async function run() {
const searchValue = process.argv[3];
const replaceValue = process.argv[4];
await fixUsersAvatar(searchValue, replaceValue);
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/getUserId.ts
================================================
import chalk from 'chalk';
import User from '@fiora/database/mongoose/models/user';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
export async function getUserId(username: string) {
if (!username) {
console.log(chalk.red('Wrong command, [username] is missing.'));
return;
}
await initMongoDB();
const user = await User.findOne({ username });
if (!user) {
console.log(chalk.red(`User [${username}] does not exist`));
} else {
console.log(
`The userId of [${username}] is:`,
chalk.green(user._id.toString()),
);
}
}
async function run() {
const username = process.argv[3];
await getUserId(username);
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/register.ts
================================================
/**
* Register
*/
import bcrypt from 'bcryptjs';
import chalk from 'chalk';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
import User, { UserDocument } from '../../database/mongoose/models/user';
import Group from '../../database/mongoose/models/group';
import { SALT_ROUNDS } from '../../utils/const';
import getRandomAvatar from '../../utils/getRandomAvatar';
export async function register(username: string, password: string) {
if (!username) {
console.log(chalk.red('Wrong command, [username] is missing.'));
return;
}
if (!password) {
console.log(chalk.red('Wrong command, [password] is missing.'));
return;
}
await initMongoDB();
const user = await User.findOne({ username });
if (user) {
console.log(chalk.red('The username already exists'));
return;
}
const defaultGroup = await Group.findOne({ isDefault: true });
if (!defaultGroup) {
console.log(chalk.red('Default group does not exist'));
return;
}
const salt = await bcrypt.genSalt(SALT_ROUNDS);
const hash = await bcrypt.hash(password, salt);
let newUser = null;
try {
newUser = await User.create({
username,
salt,
password: hash,
avatar: getRandomAvatar(),
} as UserDocument);
} catch (createError) {
if (createError.name === 'ValidationError') {
console.log(
chalk.red(
'Username contains unsupported characters or the length exceeds the limit',
),
);
return;
}
console.log(chalk.red('Error:'), createError);
return;
}
if (!defaultGroup.creator) {
defaultGroup.creator = newUser._id;
}
if (newUser) {
defaultGroup.members.push(newUser._id);
}
await defaultGroup.save();
console.log(chalk.green(`Successfully created user [${username}]`));
}
async function run() {
const username = process.argv[3];
const password = process.argv[4];
await register(username, password);
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/scripts/updateDefaultGroupName.ts
================================================
import chalk from 'chalk';
import initMongoDB from '@fiora/database/mongoose/initMongoDB';
import Group from '@fiora/database/mongoose/models/group';
export async function updateDefaultGroupName(newName: string) {
if (!newName) {
console.log(chalk.red('Wrong command, [newName] is missing.'));
return;
}
await initMongoDB();
const group = await Group.findOne({ isDefault: true });
if (!group) {
console.log(chalk.red('Default group does not exist'));
} else {
group.name = newName;
try {
await group.save();
console.log(chalk.green('Update default group name success!'));
} catch (err) {
console.log(
chalk.red('Update default group name fail!'),
err.message,
);
}
}
}
async function run() {
const newName = process.argv[3];
await updateDefaultGroupName(newName);
process.exit(0);
}
export default run;
================================================
FILE: packages/bin/tsconfig.json
================================================
{
"extends": "../../tsconfig",
}
================================================
FILE: packages/config/client.ts
================================================
import { MB } from '../utils/const';
export default {
server:
process.env.Server ||
(process.env.NODE_ENV === 'development' ? '//localhost:9200' : '/'),
maxImageSize: process.env.MaxImageSize
? parseInt(process.env.MaxImageSize, 10)
: MB * 5,
maxBackgroundImageSize: process.env.MaxBackgroundImageSize
? parseInt(process.env.MaxBackgroundImageSize, 10)
: MB * 5,
maxAvatarSize: process.env.MaxAvatarSize
? parseInt(process.env.MaxAvatarSize, 10)
: MB * 1.5,
maxFileSize: process.env.MaxFileSize
? parseInt(process.env.MaxFileSize, 10)
: MB * 10,
// client default system setting
defaultTheme: process.env.DefaultTheme || 'cool',
sound: process.env.Sound || 'default',
tagColorMode: process.env.TagColorMode || 'fixedColor',
/**
* 前端监控: https://yueying.effirst.com/index
* 值为监控应用id, 为空则不启用监控
*/
frontendMonitorAppId: process.env.FrontendMonitorAppId || '',
// 禁止用户撤回消息, 不包括管理员, 管理员始终能撤回任何消息
// 默认是禁止的
disableDeleteMessage: process.env.DisableDeleteMessage
? process.env.DisableDeleteMessage === 'true'
: false,
};
================================================
FILE: packages/config/package.json
================================================
{
"name": "@fiora/config",
"version": "1.0.0",
"license": "MIT",
"private": true,
"dependencies": {
"ip": "^1.1.5"
},
"devDependencies": {
"@types/ip": "^1.1.0"
}
}
================================================
FILE: packages/config/server.ts
================================================
import ip from 'ip';
const { env } = process;
export default {
/** 服务端host, 默认为本机ip地址(可能会是局域网地址) */
host: env.Host || ip.address(),
// service port
port: env.Port ? parseInt(env.Port, 10) : 9200,
// mongodb address
database: env.Database || 'mongodb://localhost:27017/fiora',
redis: {
host: env.RedisHost || 'localhost',
port: env.RedisPort ? parseInt(env.RedisPort, 10) : 6379,
},
// jwt encryption secret
jwtSecret: env.JwtSecret || 'jwtSecret',
gitextract_efe0p5be/ ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .github/ │ └── workflows/ │ ├── codeql-analysis.yml │ ├── lint.yml │ ├── test.yml │ └── ts.yml ├── .gitignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yaml ├── index.ts ├── jest.config.js ├── jest.setup.js ├── jest.transformer.js ├── lerna.json ├── package.json ├── packages/ │ ├── app/ │ │ ├── .babelrc │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .watchmanconfig │ │ ├── App.tsx │ │ ├── app.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── Avatar.tsx │ │ │ │ ├── BackButton.tsx │ │ │ │ ├── Expression.tsx │ │ │ │ ├── Image.tsx │ │ │ │ ├── Loading.tsx │ │ │ │ ├── Nofitication.tsx │ │ │ │ ├── PageContainer.tsx │ │ │ │ └── Toast.tsx │ │ │ ├── hooks/ │ │ │ │ └── useStore.tsx │ │ │ ├── pages/ │ │ │ │ ├── Chat/ │ │ │ │ │ ├── Chat.tsx │ │ │ │ │ ├── ChatBackButton.tsx │ │ │ │ │ ├── ChatRightButton.tsx │ │ │ │ │ ├── ImageMessage.tsx │ │ │ │ │ ├── Input.tsx │ │ │ │ │ ├── InviteMessage.tsx │ │ │ │ │ ├── Message.tsx │ │ │ │ │ ├── MessageList.tsx │ │ │ │ │ ├── SystemMessage.tsx │ │ │ │ │ └── TextMessage.tsx │ │ │ │ ├── ChatList/ │ │ │ │ │ ├── ChatList.tsx │ │ │ │ │ ├── ChatListRightButton.tsx │ │ │ │ │ ├── Linkman.tsx │ │ │ │ │ └── SelfInfo.tsx │ │ │ │ ├── GroupInfo/ │ │ │ │ │ └── GroupInfo.tsx │ │ │ │ ├── GroupProfile/ │ │ │ │ │ └── GroupProfile.tsx │ │ │ │ ├── LoginSignup/ │ │ │ │ │ ├── Base.tsx │ │ │ │ │ ├── Login.tsx │ │ │ │ │ └── Signup.tsx │ │ │ │ ├── Other/ │ │ │ │ │ ├── Other.tsx │ │ │ │ │ ├── PrivacyPolicy.tsx │ │ │ │ │ └── Sponsor.tsx │ │ │ │ ├── SearchResult/ │ │ │ │ │ └── SearchResult.tsx │ │ │ │ └── UserInfo/ │ │ │ │ └── UserInfo.tsx │ │ │ ├── service.ts │ │ │ ├── socket.ts │ │ │ ├── state/ │ │ │ │ ├── action.ts │ │ │ │ ├── reducer.ts │ │ │ │ └── store.ts │ │ │ ├── types/ │ │ │ │ ├── global.d.ts │ │ │ │ ├── redux.ts │ │ │ │ └── socket.ts │ │ │ └── utils/ │ │ │ ├── constant.ts │ │ │ ├── convertMessage.ts │ │ │ ├── expressions.ts │ │ │ ├── fetch.ts │ │ │ ├── getFriendId.ts │ │ │ ├── getRandomColor.ts │ │ │ ├── linkman.ts │ │ │ ├── platform.ts │ │ │ ├── storage.ts │ │ │ ├── time.ts │ │ │ └── uploadFile.ts │ │ ├── tests/ │ │ │ └── state/ │ │ │ └── reducer.test.ts │ │ └── tsconfig.json │ ├── assets/ │ │ └── package.json │ ├── bin/ │ │ ├── index.ts │ │ ├── package.json │ │ ├── scripts/ │ │ │ ├── deleteMessages.ts │ │ │ ├── deleteTodayRegisteredUsers.ts │ │ │ ├── deleteUser.ts │ │ │ ├── doctor.ts │ │ │ ├── fixUsersAvatar.ts │ │ │ ├── getUserId.ts │ │ │ ├── register.ts │ │ │ └── updateDefaultGroupName.ts │ │ └── tsconfig.json │ ├── config/ │ │ ├── client.ts │ │ ├── package.json │ │ └── server.ts │ ├── database/ │ │ ├── mongoose/ │ │ │ ├── index.ts │ │ │ ├── initMongoDB.ts │ │ │ └── models/ │ │ │ ├── friend.ts │ │ │ ├── group.ts │ │ │ ├── history.ts │ │ │ ├── message.ts │ │ │ ├── notification.ts │ │ │ ├── socket.ts │ │ │ └── user.ts │ │ ├── package.json │ │ ├── redis/ │ │ │ └── initRedis.ts │ │ └── tsconfig.json │ ├── docs/ │ │ ├── .gitignore │ │ ├── babel.config.js │ │ ├── docs/ │ │ │ ├── API.md │ │ │ ├── App.md │ │ │ ├── CHANGELOG.md │ │ │ ├── Config.md │ │ │ ├── FAQ.md │ │ │ ├── Getting-Start.md │ │ │ ├── INSTALL.md │ │ │ └── Script.md │ │ ├── docusaurus.config.js │ │ ├── i18n/ │ │ │ ├── en/ │ │ │ │ ├── code.json │ │ │ │ ├── docusaurus-plugin-content-docs/ │ │ │ │ │ └── current/ │ │ │ │ │ ├── API.md │ │ │ │ │ ├── App.md │ │ │ │ │ ├── CHANGELOG.md │ │ │ │ │ ├── Config.md │ │ │ │ │ ├── FAQ.md │ │ │ │ │ ├── Getting-Start.md │ │ │ │ │ ├── INSTALL.md │ │ │ │ │ └── Script.md │ │ │ │ └── docusaurus-theme-classic/ │ │ │ │ ├── footer.json │ │ │ │ └── navbar.json │ │ │ └── zh-Hans/ │ │ │ ├── code.json │ │ │ ├── docusaurus-plugin-content-docs/ │ │ │ │ └── current/ │ │ │ │ ├── API.md │ │ │ │ ├── App.md │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── Config.md │ │ │ │ ├── FAQ.md │ │ │ │ ├── Getting-Start.md │ │ │ │ ├── INSTALL.md │ │ │ │ └── Script.md │ │ │ └── docusaurus-theme-classic/ │ │ │ ├── footer.json │ │ │ └── navbar.json │ │ ├── package.json │ │ ├── sidebars.js │ │ ├── src/ │ │ │ ├── css/ │ │ │ │ └── custom.css │ │ │ └── pages/ │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ └── static/ │ │ └── .nojekyll │ ├── i18n/ │ │ ├── en-US/ │ │ │ ├── bin.ts │ │ │ └── index.ts │ │ ├── node.index.ts │ │ ├── package.json │ │ └── zh-CN/ │ │ ├── bin.ts │ │ └── index.ts │ ├── server/ │ │ ├── .nodemonrc │ │ ├── package.json │ │ ├── public/ │ │ │ ├── PrivacyPolicy.html │ │ │ ├── index.html │ │ │ └── manifest.json │ │ ├── src/ │ │ │ ├── app.ts │ │ │ ├── main.ts │ │ │ ├── middlewares/ │ │ │ │ ├── frequency.ts │ │ │ │ ├── isAdmin.ts │ │ │ │ ├── isLogin.ts │ │ │ │ ├── registerRoutes.ts │ │ │ │ └── seal.ts │ │ │ ├── routes/ │ │ │ │ ├── group.ts │ │ │ │ ├── history.ts │ │ │ │ ├── message.ts │ │ │ │ ├── notification.ts │ │ │ │ ├── system.ts │ │ │ │ └── user.ts │ │ │ └── types/ │ │ │ ├── index.d.ts │ │ │ └── server.d.ts │ │ ├── test/ │ │ │ ├── helpers/ │ │ │ │ └── middleware.ts │ │ │ └── middlewares/ │ │ │ ├── frequency.spec.ts │ │ │ ├── isAdmin.spec.ts │ │ │ ├── isLogin.spec.ts │ │ │ └── seal.spec.ts │ │ └── tsconfig.json │ ├── utils/ │ │ ├── compressImage.ts │ │ ├── const.ts │ │ ├── convertMessage.ts │ │ ├── expressions.ts │ │ ├── getFriendId.ts │ │ ├── getRandomAvatar.ts │ │ ├── getRandomColor.ts │ │ ├── logger.ts │ │ ├── package.json │ │ ├── sleep.ts │ │ ├── socket.ts │ │ ├── test/ │ │ │ ├── getFriendId.spec.ts │ │ │ └── url.spec.ts │ │ ├── time.ts │ │ ├── ua.ts │ │ ├── url.ts │ │ └── xss.ts │ └── web/ │ ├── .babelrc │ ├── build/ │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ └── webpack.prod.js │ ├── package.json │ ├── src/ │ │ ├── App.less │ │ ├── App.tsx │ │ ├── components/ │ │ │ ├── Avatar.tsx │ │ │ ├── Button.tsx │ │ │ ├── Dialog.less │ │ │ ├── Dialog.tsx │ │ │ ├── Dropdown.less │ │ │ ├── Dropdown.tsx │ │ │ ├── IconButton.less │ │ │ ├── IconButton.tsx │ │ │ ├── Input.less │ │ │ ├── Input.tsx │ │ │ ├── Loading.tsx │ │ │ ├── Menu.tsx │ │ │ ├── Message.less │ │ │ ├── Message.tsx │ │ │ ├── Progress.tsx │ │ │ ├── Select.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── Tooltip.less │ │ │ └── Tooltip.tsx │ │ ├── context.ts │ │ ├── globalStyles.ts │ │ ├── hooks/ │ │ │ ├── useAction.ts │ │ │ ├── useAero.ts │ │ │ ├── useIsLogin.ts │ │ │ └── useStore.ts │ │ ├── localStorage.ts │ │ ├── main.tsx │ │ ├── modules/ │ │ │ ├── Chat/ │ │ │ │ ├── Chat.less │ │ │ │ ├── Chat.tsx │ │ │ │ ├── ChatInput.less │ │ │ │ ├── ChatInput.tsx │ │ │ │ ├── CodeEditor.less │ │ │ │ ├── CodeEditor.tsx │ │ │ │ ├── Expression.less │ │ │ │ ├── Expression.tsx │ │ │ │ ├── GroupManagePanel.less │ │ │ │ ├── GroupManagePanel.tsx │ │ │ │ ├── HeaderBar.less │ │ │ │ ├── HeaderBar.tsx │ │ │ │ ├── Message/ │ │ │ │ │ ├── CodeDialog.tsx │ │ │ │ │ ├── CodeMessage.less │ │ │ │ │ ├── CodeMessage.tsx │ │ │ │ │ ├── FileMessage.tsx │ │ │ │ │ ├── ImageMessage.tsx │ │ │ │ │ ├── InviteMessage.less │ │ │ │ │ ├── InviteMessageV2.tsx │ │ │ │ │ ├── Message.less │ │ │ │ │ ├── Message.tsx │ │ │ │ │ ├── SystemMessage.tsx │ │ │ │ │ ├── TextMessage.tsx │ │ │ │ │ └── UrlMessage.tsx │ │ │ │ ├── MessageList.less │ │ │ │ └── MessageList.tsx │ │ │ ├── FunctionBarAndLinkmanList/ │ │ │ │ ├── CreateGroup.less │ │ │ │ ├── CreateGroup.tsx │ │ │ │ ├── FunctionBar.less │ │ │ │ ├── FunctionBar.tsx │ │ │ │ ├── FunctionBarAndLinkmanList.less │ │ │ │ ├── FunctionBarAndLinkmanList.tsx │ │ │ │ ├── Linkman.less │ │ │ │ ├── Linkman.tsx │ │ │ │ ├── LinkmanList.less │ │ │ │ └── LinkmanList.tsx │ │ │ ├── GroupInfo.tsx │ │ │ ├── InfoDialog.less │ │ │ ├── InviteInfo.tsx │ │ │ ├── LoginAndRegister/ │ │ │ │ ├── Login.tsx │ │ │ │ ├── LoginAndRegister.less │ │ │ │ ├── LoginAndRegister.tsx │ │ │ │ ├── LoginRegister.less │ │ │ │ └── Register.tsx │ │ │ ├── Sidebar/ │ │ │ │ ├── About.less │ │ │ │ ├── About.tsx │ │ │ │ ├── Admin.less │ │ │ │ ├── Admin.tsx │ │ │ │ ├── Common.less │ │ │ │ ├── Download.less │ │ │ │ ├── Download.tsx │ │ │ │ ├── OnlineStatus.less │ │ │ │ ├── OnlineStatus.tsx │ │ │ │ ├── Reward.less │ │ │ │ ├── Reward.tsx │ │ │ │ ├── SelfInfo.less │ │ │ │ ├── SelfInfo.tsx │ │ │ │ ├── Setting.less │ │ │ │ ├── Setting.tsx │ │ │ │ ├── Sidebar.less │ │ │ │ └── Sidebar.tsx │ │ │ └── UserInfo.tsx │ │ ├── service.ts │ │ ├── socket.ts │ │ ├── state/ │ │ │ ├── action.ts │ │ │ ├── reducer.ts │ │ │ └── store.ts │ │ ├── styles/ │ │ │ ├── iconfont.less │ │ │ ├── normalize.less │ │ │ └── variable.less │ │ ├── template.html │ │ ├── themes.ts │ │ ├── types/ │ │ │ └── index.d.ts │ │ └── utils/ │ │ ├── fetch.ts │ │ ├── getRandomHuaji.ts │ │ ├── inobounce.ts │ │ ├── notification.ts │ │ ├── playSound.ts │ │ ├── readDiskFile.ts │ │ ├── setCssVariable.ts │ │ ├── uploadFile.ts │ │ └── voice.ts │ ├── test/ │ │ ├── components/ │ │ │ ├── Avatar.spec.tsx │ │ │ └── Button.spec.tsx │ │ ├── localStorage.spec.ts │ │ └── state/ │ │ └── reducer.spec.ts │ └── tsconfig.json └── tsconfig.json
SYMBOL INDEX (499 symbols across 155 files)
FILE: index.ts
function exec (line 7) | function exec(commandStr: string) {
FILE: jest.transformer.js
method process (line 4) | process(src, filename) {
FILE: packages/app/App.tsx
function Main (line 7) | function Main(props: any) {
FILE: packages/app/src/App.tsx
type Props (line 26) | type Props = {
function App (line 32) | function App({ title, primaryColor, isLogin }: Props) {
FILE: packages/app/src/components/Avatar.tsx
type Props (line 6) | type Props = {
function Avatar (line 10) | function Avatar({ src, size }: Props) {
FILE: packages/app/src/components/BackButton.tsx
type Props (line 6) | type Props = {
function BackButton (line 10) | function BackButton({ text = '' }: Props) {
FILE: packages/app/src/components/Expression.tsx
type Props (line 7) | type Props = {
function Expression (line 13) | function Expression({ size, index, style }: Props) {
FILE: packages/app/src/components/Image.tsx
type Props (line 6) | type Props = {
function Image (line 13) | function Image({
FILE: packages/app/src/components/Loading.tsx
function Loading (line 8) | function Loading() {
FILE: packages/app/src/components/Nofitication.tsx
function enableNotification (line 13) | function enableNotification() {
function disableNotification (line 22) | function disableNotification() {
function Nofitication (line 32) | function Nofitication() {
FILE: packages/app/src/components/PageContainer.tsx
type Props (line 5) | type Props = {
function PageContainer (line 10) | function PageContainer({ children, disableSafeAreaView = false }: Props) {
FILE: packages/app/src/components/Toast.tsx
method success (line 4) | success(message: string) {
method warning (line 11) | warning(message: string) {
method danger (line 18) | danger(message: string) {
FILE: packages/app/src/hooks/useStore.tsx
function useStore (line 4) | function useStore() {
function useUser (line 8) | function useUser() {
function useSelfId (line 12) | function useSelfId() {
function useIsLogin (line 17) | function useIsLogin() {
function useIsAdmin (line 21) | function useIsAdmin() {
function useTheme (line 26) | function useTheme() {
function useLinkmans (line 36) | function useLinkmans() {
function useFocusLinkman (line 41) | function useFocusLinkman() {
function useFocus (line 50) | function useFocus() {
FILE: packages/app/src/pages/Chat/Chat.tsx
function Chat (line 64) | function Chat() {
FILE: packages/app/src/pages/Chat/ChatBackButton.tsx
function ChatBackButton (line 5) | function ChatBackButton() {
FILE: packages/app/src/pages/Chat/ChatRightButton.tsx
function ChatRightButton (line 7) | function ChatRightButton() {
FILE: packages/app/src/pages/Chat/ImageMessage.tsx
type Props (line 10) | type Props = {
function ImageMessage (line 17) | function ImageMessage({
FILE: packages/app/src/pages/Chat/Input.tsx
type Props (line 29) | type Props = {
function Input (line 33) | function Input({ onHeightChange }: Props) {
FILE: packages/app/src/pages/Chat/InviteMessage.tsx
type Props (line 10) | type Props = {
function InviteMessage (line 15) | function InviteMessage({ message, isSelf }: Props) {
FILE: packages/app/src/pages/Chat/Message.tsx
type Props (line 32) | type Props = {
function Message (line 40) | function Message({
FILE: packages/app/src/pages/Chat/MessageList.tsx
type Props (line 20) | type Props = {
function MessageList (line 29) | function MessageList({ $scrollView }: Props) {
FILE: packages/app/src/pages/Chat/SystemMessage.tsx
type Props (line 7) | type Props = {
function SystemMessage (line 11) | function SystemMessage({ message }: Props) {
FILE: packages/app/src/pages/Chat/TextMessage.tsx
type Props (line 8) | type Props = {
function TextMessage (line 13) | function TextMessage({ message, isSelf }: Props) {
FILE: packages/app/src/pages/ChatList/ChatList.tsx
function ChatList (line 13) | function ChatList() {
FILE: packages/app/src/pages/ChatList/ChatListRightButton.tsx
function ChatListRightButton (line 9) | function ChatListRightButton() {
FILE: packages/app/src/pages/ChatList/Linkman.tsx
type Props (line 13) | type Props = {
function Linkman (line 24) | function Linkman({
FILE: packages/app/src/pages/ChatList/SelfInfo.tsx
function SelfInfo (line 7) | function SelfInfo() {
FILE: packages/app/src/pages/GroupInfo/GroupInfo.tsx
type Props (line 12) | type Props = {
function GroupInfo (line 21) | function GroupInfo({ group }: Props) {
FILE: packages/app/src/pages/GroupProfile/GroupProfile.tsx
function GroupProfile (line 12) | function GroupProfile() {
FILE: packages/app/src/pages/LoginSignup/Base.tsx
type Props (line 8) | type Props = {
function Base (line 15) | function Base({
FILE: packages/app/src/pages/LoginSignup/Login.tsx
function Login (line 13) | function Login() {
FILE: packages/app/src/pages/LoginSignup/Signup.tsx
function Signup (line 13) | function Signup() {
FILE: packages/app/src/pages/Other/Other.tsx
function getIsNight (line 26) | function getIsNight() {
function Other (line 31) | function Other() {
FILE: packages/app/src/pages/Other/PrivacyPolicy.tsx
type Props (line 9) | type Props = {
function PrivacyPolicy (line 14) | function PrivacyPolicy({ visible, onClose }: Props) {
FILE: packages/app/src/pages/Other/Sponsor.tsx
type Props (line 6) | type Props = {
function Sponsor (line 12) | function Sponsor({ visible, onClose, onOK }: Props) {
FILE: packages/app/src/pages/SearchResult/SearchResult.tsx
type Props (line 8) | type Props = {
function SearchResult (line 22) | function SearchResult({ groups, users }: Props) {
FILE: packages/app/src/pages/UserInfo/UserInfo.tsx
type Props (line 25) | type Props = {
function UserInfo (line 34) | function UserInfo({ user }: Props) {
FILE: packages/app/src/service.ts
function saveUsername (line 4) | function saveUsername(username: string) {
function register (line 16) | async function register(
function login (line 47) | async function login(
function loginByToken (line 77) | async function loginByToken(
function guest (line 108) | async function guest(os = '', browser = '', environment = '') {
function changeAvatar (line 120) | async function changeAvatar(avatar: string) {
function changePassword (line 130) | async function changePassword(oldPassword: string, newPassword: string) {
function changeUsername (line 142) | async function changeUsername(username: string) {
function changeGroupName (line 154) | async function changeGroupName(groupId: string, name: string) {
function changeGroupAvatar (line 164) | async function changeGroupAvatar(groupId: string, avatar: string) {
function createGroup (line 173) | async function createGroup(name: string) {
function deleteGroup (line 182) | async function deleteGroup(groupId: string) {
function joinGroup (line 191) | async function joinGroup(groupId: string) {
function leaveGroup (line 200) | async function leaveGroup(groupId: string) {
function addFriend (line 209) | async function addFriend(userId: string) {
function deleteFriend (line 218) | async function deleteFriend(userId: string) {
function getLinkmanHistoryMessages (line 228) | async function getLinkmanHistoryMessages(
function getDefaultGroupHistoryMessages (line 243) | async function getDefaultGroupHistoryMessages(existCount: number) {
function search (line 254) | async function search(keywords: string) {
function searchExpression (line 263) | async function searchExpression(keywords: string) {
function sendMessage (line 274) | async function sendMessage(to: string, type: string, content: string) {
function deleteMessage (line 282) | async function deleteMessage(messageId: string) {
function getGroupOnlineMembers (line 291) | async function getGroupOnlineMembers(groupId: string) {
function getDefaultGroupOnlineMembers (line 299) | async function getDefaultGroupOnlineMembers() {
function sealUser (line 308) | async function sealUser(username: string) {
function sealIp (line 317) | async function sealIp(ip: string) {
function sealUserOnlineIp (line 326) | async function sealUserOnlineIp(userId: string) {
function getSealList (line 334) | async function getSealList() {
function resetUserPassword (line 343) | async function resetUserPassword(username: string) {
function setUserTag (line 353) | async function setUserTag(username: string, tag: string) {
function getUserIps (line 362) | async function getUserIps(userId: string) {
function getUserOnlineStatus (line 367) | async function getUserOnlineStatus(userId: string) {
function setNotificationToken (line 372) | async function setNotificationToken(token: string) {
FILE: packages/app/src/socket.ts
function fetch (line 44) | function fetch<T = any>(
function guest (line 63) | async function guest() {
FILE: packages/app/src/state/action.ts
function connect (line 41) | function connect() {
function disconnect (line 47) | function disconnect() {
function setUser (line 54) | function setUser(user: any) {
function setLinkmansLastMessages (line 86) | function setLinkmansLastMessages(
function setGuest (line 94) | function setGuest(defaultGroup: Group) {
function logout (line 106) | function logout() {
function setAvatar (line 111) | function setAvatar(avatar: string) {
function updateUserProperty (line 118) | function updateUserProperty(key: string, value: any) {
function addLinkmanMessage (line 126) | function addLinkmanMessage(linkmanId: string, message: Message) {
function deleteLinkmanMessage (line 134) | function deleteLinkmanMessage(linkmanId: string, messageId: string) {
function addLinkmanHistoryMessages (line 142) | function addLinkmanHistoryMessages(linkmanId: string, messages: Message[...
function updateSelfMessage (line 149) | function updateSelfMessage(
function setFocus (line 162) | function setFocus(linkmanId: string) {
function setGroupMembers (line 168) | function setGroupMembers(groupId: string, members: Group['members']) {
function setGroupAvatar (line 176) | function setGroupAvatar(groupId: string, avatar: string) {
function updateGroupProperty (line 184) | function updateGroupProperty(groupId: string, key: string, value: any) {
function updateFriendProperty (line 192) | function updateFriendProperty(userId: string, key: string, value: any) {
function addLinkman (line 200) | function addLinkman(linkman: Linkman, focus = false) {
function removeLinkman (line 212) | function removeLinkman(linkmanId: string) {
function setFriend (line 218) | function setFriend(linkmanId: string, from: Friend['from'], to: Friend['...
function loading (line 227) | function loading(text: string) {
FILE: packages/app/src/state/reducer.ts
function mergeLinkmans (line 30) | function mergeLinkmans(
FILE: packages/app/src/types/redux.ts
type ConnectAction (line 2) | type ConnectAction = {
type SetUserAction (line 8) | type SetUserAction = {
type SetLinkmanMessagesAction (line 15) | type SetLinkmanMessagesAction = {
type SetGuestAction (line 27) | type SetGuestAction = {
type LogoutAction (line 33) | type LogoutAction = {
type UpdateUserPropertyAction (line 38) | type UpdateUserPropertyAction = {
type AddlinkmanMessageAction (line 45) | type AddlinkmanMessageAction = {
type DeleteLinkmanMessageAction (line 52) | type DeleteLinkmanMessageAction = {
type AddLinkmanHistoryMessagesAction (line 59) | type AddLinkmanHistoryMessagesAction = {
type UpdateSelfMessageAction (line 66) | type UpdateSelfMessageAction = {
type SetFocusAction (line 74) | type SetFocusAction = {
type UpdateGroupPropertyAction (line 80) | type UpdateGroupPropertyAction = {
type UpdateFriendPropertyAction (line 88) | type UpdateFriendPropertyAction = {
type AddLinkmanAction (line 96) | type AddLinkmanAction = {
type RemoveLinkmanAction (line 103) | type RemoveLinkmanAction = {
type SetFriendAction (line 109) | type SetFriendAction = {
type UpdateUIPropertyAction (line 117) | type UpdateUIPropertyAction = {
type ActionTypes (line 123) | type ActionTypes =
type Message (line 142) | type Message = {
type Group (line 160) | type Group = {
type Friend (line 182) | type Friend = {
type Temporary (line 199) | type Temporary = {
type Linkman (line 210) | type Linkman = Group | Friend | Temporary;
type User (line 212) | type User = {
type State (line 222) | type State = {
FILE: packages/app/src/types/socket.ts
type Socket (line 1) | type Socket = {
FILE: packages/app/src/utils/convertMessage.ts
function convertSystemMessage (line 20) | function convertSystemMessage(message: Message) {
function convertMessageHtml (line 66) | function convertMessageHtml(message: Message) {
function convertMessage (line 75) | function convertMessage(message: Message) {
FILE: packages/app/src/utils/fetch.ts
function fetch (line 4) | function fetch<T = any>(
FILE: packages/app/src/utils/getFriendId.ts
function getFriendId (line 1) | function getFriendId(userId1: string, userId2: string) {
FILE: packages/app/src/utils/getRandomColor.ts
type ColorMode (line 3) | type ColorMode = 'dark' | 'bright' | 'light' | 'random';
function getRandomColor (line 9) | function getRandomColor(seed: string, luminosity: ColorMode = 'dark') {
type Cache (line 16) | type Cache = {
function getPerRandomColor (line 27) | function getPerRandomColor(
FILE: packages/app/src/utils/linkman.ts
function formatLinkmanName (line 3) | function formatLinkmanName(linkman: Linkman) {
FILE: packages/app/src/utils/storage.ts
function getStorageValue (line 3) | async function getStorageValue(key: string) {
function setStorageValue (line 7) | async function setStorageValue(key: string, value: string) {
function removeStorageValue (line 11) | async function removeStorageValue(key: string) {
FILE: packages/app/src/utils/time.ts
method isToday (line 2) | isToday(time1: Date, time2: Date) {
method isYesterday (line 9) | isYesterday(time1: Date, time2: Date) {
method isSameYear (line 18) | isSameYear(time1: Date, time2: Date) {
method getHourMinute (line 21) | getHourMinute(time: Date) {
method getMonthDate (line 28) | getMonthDate(time: Date) {
method getYearMonthDate (line 31) | getYearMonthDate(time: Date) {
FILE: packages/app/src/utils/uploadFile.ts
function uploadFile (line 8) | async function uploadFile(
function getOSSFileUrl (line 24) | function getOSSFileUrl(url: string | number = '', process = '') {
FILE: packages/bin/scripts/deleteMessages.ts
function deleteMessages (line 10) | async function deleteMessages() {
function run (line 57) | async function run() {
FILE: packages/bin/scripts/deleteTodayRegisteredUsers.ts
function deleteTodayRegisteredUsers (line 11) | async function deleteTodayRegisteredUsers() {
function run (line 49) | async function run() {
FILE: packages/bin/scripts/deleteUser.ts
function deleteUser (line 12) | async function deleteUser(userIdOrName: string, confirm = true) {
function run (line 112) | async function run() {
FILE: packages/bin/scripts/doctor.ts
function doctor (line 10) | async function doctor() {
function run (line 53) | async function run() {
FILE: packages/bin/scripts/fixUsersAvatar.ts
function fixUsersAvatar (line 6) | async function fixUsersAvatar(
function run (line 44) | async function run() {
FILE: packages/bin/scripts/getUserId.ts
function getUserId (line 5) | async function getUserId(username: string) {
function run (line 24) | async function run() {
FILE: packages/bin/scripts/register.ts
function register (line 15) | async function register(username: string, password: string) {
function run (line 74) | async function run() {
FILE: packages/bin/scripts/updateDefaultGroupName.ts
function updateDefaultGroupName (line 5) | async function updateDefaultGroupName(newName: string) {
function run (line 30) | async function run() {
FILE: packages/database/mongoose/initMongoDB.ts
function initMongoDB (line 13) | function initMongoDB() {
FILE: packages/database/mongoose/models/friend.ts
type FriendDocument (line 17) | interface FriendDocument extends Document {
FILE: packages/database/mongoose/models/group.ts
type GroupDocument (line 35) | interface GroupDocument extends Document {
FILE: packages/database/mongoose/models/history.ts
type HistoryDocument (line 18) | interface HistoryDocument extends Document {
function createOrUpdateHistory (line 33) | async function createOrUpdateHistory(
FILE: packages/database/mongoose/models/message.ts
type MessageDocument (line 31) | interface MessageDocument extends Document {
type SendMessageData (line 54) | interface SendMessageData {
function handleInviteV2Message (line 60) | async function handleInviteV2Message(message: SendMessageData) {
function handleInviteV2Messages (line 80) | async function handleInviteV2Messages(messages: SendMessageData[]) {
FILE: packages/database/mongoose/models/notification.ts
type NotificationDocument (line 16) | interface NotificationDocument extends Document {
FILE: packages/database/mongoose/models/socket.ts
type SocketDocument (line 30) | interface SocketDocument extends Document {
FILE: packages/database/mongoose/models/user.ts
type UserDocument (line 32) | interface UserDocument extends Document {
FILE: packages/database/redis/initRedis.ts
function initRedis (line 6) | function initRedis() {
function set (line 25) | async function set(key: string, value: string, expireTime = Infinity) {
function has (line 34) | async function has(key: string) {
function getNewUserKey (line 39) | function getNewUserKey(userId: string) {
function getNewRegisteredUserIpKey (line 43) | function getNewRegisteredUserIpKey(ip: string) {
function getSealIpKey (line 49) | function getSealIpKey(ip: string) {
function getAllSealIp (line 53) | async function getAllSealIp() {
function getSealUserKey (line 58) | function getSealUserKey(user: string) {
function getAllSealUser (line 62) | async function getAllSealUser() {
FILE: packages/docs/src/pages/index.js
function Feature (line 34) | function Feature({ imageUrl, title, description }) {
function Description (line 70) | function Description({ title, content, image, index }) {
function DeployByYourself (line 86) | function DeployByYourself({ url }) {
function Home (line 105) | function Home() {
FILE: packages/i18n/node.index.ts
function i18n (line 13) | function i18n(key: keyof typeof enUS | keyof typeof zhCN) {
FILE: packages/server/src/middlewares/frequency.ts
constant CALL_SERVICE_FREQUENTLY (line 8) | const CALL_SERVICE_FREQUENTLY = '发消息过于频繁, 请冷静一会再试';
constant NEW_USER_CALL_SERVICE_FREQUENTLY (line 9) | const NEW_USER_CALL_SERVICE_FREQUENTLY =
type Options (line 18) | type Options = {
function frequency (line 28) | function frequency(
FILE: packages/server/src/middlewares/isAdmin.ts
constant YOU_ARE_NOT_ADMINISTRATOR (line 4) | const YOU_ARE_NOT_ADMINISTRATOR = '你不是管理员';
function isAdmin (line 9) | function isAdmin(socket: Socket) {
FILE: packages/server/src/middlewares/isLogin.ts
constant PLEASE_LOGIN (line 3) | const PLEASE_LOGIN = '请登录后再试';
function isLogin (line 8) | function isLogin(socket: Socket) {
FILE: packages/server/src/middlewares/registerRoutes.ts
function defaultCallback (line 6) | function defaultCallback() {
function registerRoutes (line 10) | function registerRoutes(socket: Socket, routes: Routes) {
FILE: packages/server/src/middlewares/seal.ts
function seal (line 13) | function seal(socket: Socket) {
FILE: packages/server/src/routes/group.ts
function getGroupOnlineMembersHelper (line 17) | async function getGroupOnlineMembersHelper(group: GroupDocument) {
function createGroup (line 42) | async function createGroup(ctx: Context<{ name: string }>) {
function joinGroup (line 86) | async function joinGroup(ctx: Context<{ groupId: string }>) {
function leaveGroup (line 127) | async function leaveGroup(ctx: Context<{ groupId: string }>) {
function getGroupOnlineMembersWrapperV2 (line 160) | function getGroupOnlineMembersWrapperV2() {
function getGroupOnlineMembers (line 212) | async function getGroupOnlineMembers(
function getDefaultGroupOnlineMembersWrapper (line 223) | function getDefaultGroupOnlineMembersWrapper() {
function changeGroupAvatar (line 246) | async function changeGroupAvatar(
function changeGroupName (line 270) | async function changeGroupName(
function deleteGroup (line 301) | async function deleteGroup(ctx: Context<{ groupId: string }>) {
function getGroupBasicInfo (line 322) | async function getGroupBasicInfo(ctx: Context<{ groupId: string }>) {
FILE: packages/server/src/routes/history.ts
function updateHistory (line 8) | async function updateHistory(
FILE: packages/server/src/routes/message.ts
constant RPS (line 39) | const RPS = ['石头', '剪刀', '布'];
function pushNotification (line 41) | async function pushNotification(
function sendMessage (line 83) | async function sendMessage(ctx: Context<SendMessageData>) {
function getLinkmansLastMessages (line 247) | async function getLinkmansLastMessages(
function getLinkmansLastMessagesV2 (line 280) | async function getLinkmansLastMessagesV2(
function getLinkmanHistoryMessages (line 354) | async function getLinkmanHistoryMessages(
function getDefaultGroupHistoryMessages (line 382) | async function getDefaultGroupHistoryMessages(
function deleteMessage (line 413) | async function deleteMessage(ctx: Context<{ messageId: string }>) {
FILE: packages/server/src/routes/notification.ts
function setNotificationToken (line 5) | async function setNotificationToken(ctx: Context<{ token: string }>) {
FILE: packages/server/src/routes/system.ts
function search (line 34) | async function search(ctx: Context<{ keywords: string }>) {
function searchExpression (line 68) | async function searchExpression(
function getBaiduToken (line 127) | async function getBaiduToken() {
function sealUser (line 147) | async function sealUser(ctx: Context<{ username: string }>) {
function getSealList (line 170) | async function getSealList() {
function sealIp (line 189) | async function sealIp(ctx: Context<{ ip: string }>) {
function sealUserOnlineIp (line 207) | async function sealUserOnlineIp(ctx: Context<{ userId: string }>) {
type STSResult (line 241) | type STSResult = {
function getSTS (line 252) | async function getSTS(): Promise<STSResult> {
function uploadFile (line 285) | async function uploadFile(
function toggleSendMessage (line 332) | async function toggleSendMessage(ctx: Context<{ enable: boolean }>) {
function toggleNewUserSendMessage (line 340) | async function toggleNewUserSendMessage(
function getSystemConfig (line 350) | async function getSystemConfig() {
FILE: packages/server/src/routes/user.ts
type Environment (line 28) | interface Environment {
function generateToken (line 42) | function generateToken(user: string, environment: string) {
function handleNewUser (line 57) | async function handleNewUser(user: UserDocument, ip = '') {
function getUserNotificationTokens (line 76) | async function getUserNotificationTokens(user: UserDocument) {
function register (line 85) | async function register(
function login (line 173) | async function login(
function loginByToken (line 245) | async function loginByToken(
function guest (line 329) | async function guest(ctx: Context<Environment>) {
function changeAvatar (line 377) | async function changeAvatar(ctx: Context<{ avatar: string }>) {
function addFriend (line 395) | async function addFriend(ctx: Context<{ userId: string }>) {
function deleteFriend (line 426) | async function deleteFriend(ctx: Context<{ userId: string }>) {
function changePassword (line 443) | async function changePassword(
function changeUsername (line 472) | async function changeUsername(ctx: Context<{ username: string }>) {
function resetUserPassword (line 496) | async function resetUserPassword(ctx: Context<{ username: string }>) {
function setUserTag (line 522) | async function setUserTag(
function getUserIps (line 555) | async function getUserIps(
function getUserOnlineStatusWrapper (line 568) | function getUserOnlineStatusWrapper() {
FILE: packages/server/src/types/server.d.ts
type Context (line 1) | interface Context<T> {
type RouteHandler (line 14) | interface RouteHandler {
type Routes (line 18) | type Routes = Record<string, RouteHandler | null>;
type MiddlewareArgs (line 20) | type MiddlewareArgs = Array<any>;
type MiddlewareNext (line 22) | type MiddlewareNext = () => void;
type SendMessageData (line 24) | interface SendMessageData {
FILE: packages/server/test/helpers/middleware.ts
function getMiddlewareParams (line 1) | function getMiddlewareParams(event = 'login', data = {}) {
FILE: packages/utils/compressImage.ts
function compressImage (line 7) | function compressImage(
FILE: packages/utils/const.ts
constant SEAL_TEXT (line 2) | const SEAL_TEXT = '你已经被关进小黑屋中, 请反思后再试';
constant SEAL_USER_TIMEOUT (line 5) | const SEAL_USER_TIMEOUT = 1000 * 60 * 10;
constant SEAL_IP_TIMEOUT (line 8) | const SEAL_IP_TIMEOUT = 1000 * 60 * 60 * 6;
constant TRANSPARENT_IMAGE (line 11) | const TRANSPARENT_IMAGE =
constant SALT_ROUNDS (line 15) | const SALT_ROUNDS = 10;
constant NAME_REGEXP (line 19) | const NAME_REGEXP = /^([0-9a-zA-Z]{1,2}|[\u4e00-\u9eff]|[\u3040-\u309Fー]...
FILE: packages/utils/convertMessage.ts
function convertSystemMessage (line 18) | function convertSystemMessage(message: any) {
function convertMessage (line 51) | function convertMessage(message: any) {
FILE: packages/utils/getFriendId.ts
function getFriendId (line 7) | function getFriendId(userId1: string, userId2: string) {
FILE: packages/utils/getRandomAvatar.ts
function getRandomAvatar (line 7) | function getRandomAvatar() {
function getDefaultAvatar (line 15) | function getDefaultAvatar() {
FILE: packages/utils/getRandomColor.ts
type ColorMode (line 3) | type ColorMode = 'dark' | 'bright' | 'light' | 'random';
function getRandomColor (line 9) | function getRandomColor(seed: string, luminosity: ColorMode = 'dark') {
type Cache (line 16) | type Cache = {
function getPerRandomColor (line 27) | function getPerRandomColor(
FILE: packages/utils/sleep.ts
function sleep (line 1) | function sleep(duration = 200) {
FILE: packages/utils/socket.ts
function getSocketIp (line 3) | function getSocketIp(socket: Socket) {
FILE: packages/utils/time.ts
method isToday (line 2) | isToday(time1: Date, time2: Date) {
method isYesterday (line 9) | isYesterday(time1: Date, time2: Date) {
method getHourMinute (line 18) | getHourMinute(time: Date) {
method getMonthDate (line 25) | getMonthDate(time: Date) {
FILE: packages/utils/url.ts
type UrlParams (line 1) | interface UrlParams {
function addParam (line 6) | function addParam(url: string, params: UrlParams) {
FILE: packages/utils/xss.ts
function processXss (line 7) | function processXss(text: string) {
FILE: packages/web/src/App.tsx
function getWidthPercent (line 26) | function getWidthPercent() {
function getHeightPercent (line 45) | function getHeightPercent() {
function App (line 57) | function App() {
FILE: packages/web/src/components/Avatar.tsx
type Props (line 6) | type Props = {
function Avatar (line 19) | function Avatar({
FILE: packages/web/src/components/Button.tsx
type Props (line 19) | type Props = {
function Button (line 29) | function Button({
FILE: packages/web/src/components/IconButton.tsx
type Props (line 5) | type Props = {
function IconButton (line 15) | function IconButton({
FILE: packages/web/src/components/Input.tsx
type InputProps (line 6) | interface InputProps {
function Input (line 16) | function Input(props: InputProps) {
FILE: packages/web/src/components/Message.tsx
function showMessage (line 7) | function showMessage(text: string, duration = 1500, type = 'success') {
method success (line 22) | success(text: string, duration = 1.5) {
method error (line 25) | error(text: string, duration = 1.5) {
method warning (line 28) | warning(text: string, duration = 1.5) {
method info (line 31) | info(text: string, duration = 1.5) {
FILE: packages/web/src/hooks/useAction.ts
function useAction (line 9) | function useAction() {
FILE: packages/web/src/hooks/useAero.ts
function useAero (line 7) | function useAero() {
FILE: packages/web/src/hooks/useIsLogin.ts
function useIsLogin (line 7) | function useIsLogin() {
FILE: packages/web/src/hooks/useStore.ts
function useStore (line 4) | function useStore() {
function useFocusLinkman (line 8) | function useFocusLinkman(): Linkman | null {
function useSelfId (line 17) | function useSelfId() {
FILE: packages/web/src/localStorage.ts
type LocalStorageKey (line 5) | enum LocalStorageKey {
function getTextValue (line 25) | function getTextValue(key: string, defaultValue: string) {
function getSwitchValue (line 35) | function getSwitchValue(key: string, defaultValue: boolean = true) {
function getData (line 43) | function getData() {
FILE: packages/web/src/modules/Chat/Chat.tsx
function Chat (line 23) | function Chat() {
FILE: packages/web/src/modules/Chat/ChatInput.tsx
function ChatInput (line 62) | function ChatInput() {
FILE: packages/web/src/modules/Chat/CodeEditor.tsx
type LoadedLanguage (line 28) | interface LoadedLanguage {
type CodeEditorProps (line 33) | interface CodeEditorProps {
function CodeEditor (line 39) | function CodeEditor(props: CodeEditorProps) {
FILE: packages/web/src/modules/Chat/Expression.tsx
type ExpressionProps (line 19) | interface ExpressionProps {
function Expression (line 24) | function Expression(props: ExpressionProps) {
FILE: packages/web/src/modules/Chat/GroupManagePanel.tsx
type GroupManagePanelProps (line 25) | interface GroupManagePanelProps {
function GroupManagePanel (line 34) | function GroupManagePanel(props: GroupManagePanelProps) {
FILE: packages/web/src/modules/Chat/HeaderBar.tsx
type Props (line 25) | type Props = {
function HeaderBar (line 37) | function HeaderBar(props: Props) {
FILE: packages/web/src/modules/Chat/Message/CodeDialog.tsx
type CodeDialogProps (line 8) | interface CodeDialogProps {
function CodeDialog (line 15) | function CodeDialog(props: CodeDialogProps) {
FILE: packages/web/src/modules/Chat/Message/CodeMessage.tsx
type LanguageMap (line 11) | type LanguageMap = {
type CodeMessageProps (line 32) | interface CodeMessageProps {
function CodeMessage (line 36) | function CodeMessage(props: CodeMessageProps) {
FILE: packages/web/src/modules/Chat/Message/FileMessage.tsx
type Props (line 35) | type Props = {
function FileMessage (line 40) | function FileMessage({ file, percent }: Props) {
FILE: packages/web/src/modules/Chat/Message/ImageMessage.tsx
type ImageMessageProps (line 15) | interface ImageMessageProps {
function ImageMessage (line 21) | function ImageMessage(props: ImageMessageProps) {
FILE: packages/web/src/modules/Chat/Message/InviteMessageV2.tsx
type InviteMessageProps (line 8) | interface InviteMessageProps {
function InviteMessage (line 12) | function InviteMessage(props: InviteMessageProps) {
FILE: packages/web/src/modules/Chat/Message/Message.tsx
type MessageProps (line 28) | interface MessageProps {
type MessageState (line 47) | interface MessageState {
class Message (line 56) | @pureRender
method constructor (line 60) | constructor(props: MessageProps) {
method componentDidMount (line 67) | componentDidMount() {
method handleClickAvatar (line 123) | handleClickAvatar(showUserInfo: (userinfo: any) => void) {
method formatTime (line 134) | formatTime() {
method renderContent (line 149) | renderContent() {
method render (line 189) | render() {
FILE: packages/web/src/modules/Chat/Message/SystemMessage.tsx
type SystemMessageProps (line 4) | interface SystemMessageProps {
function SystemMessage (line 9) | function SystemMessage(props: SystemMessageProps) {
FILE: packages/web/src/modules/Chat/Message/TextMessage.tsx
type TextMessageProps (line 7) | interface TextMessageProps {
function TextMessage (line 11) | function TextMessage(props: TextMessageProps) {
FILE: packages/web/src/modules/Chat/Message/UrlMessage.tsx
type UrlMessageProps (line 3) | interface UrlMessageProps {
function UrlMessage (line 7) | function UrlMessage(props: UrlMessageProps) {
FILE: packages/web/src/modules/Chat/MessageList.tsx
function MessageList (line 40) | function MessageList() {
FILE: packages/web/src/modules/FunctionBarAndLinkmanList/CreateGroup.tsx
type CreateGroupProps (line 10) | interface CreateGroupProps {
function CreateGroup (line 15) | function CreateGroup(props: CreateGroupProps) {
FILE: packages/web/src/modules/FunctionBarAndLinkmanList/FunctionBar.tsx
type SearchResult (line 19) | type SearchResult = {
function FunctionBar (line 24) | function FunctionBar() {
FILE: packages/web/src/modules/FunctionBarAndLinkmanList/FunctionBarAndLinkmanList.tsx
function FunctionBarAndLinkmanList (line 13) | function FunctionBarAndLinkmanList() {
FILE: packages/web/src/modules/FunctionBarAndLinkmanList/Linkman.tsx
type LinkmanProps (line 15) | interface LinkmanProps {
function Linkman (line 25) | function Linkman(props: LinkmanProps) {
FILE: packages/web/src/modules/FunctionBarAndLinkmanList/LinkmanList.tsx
function LinkmanList (line 9) | function LinkmanList() {
FILE: packages/web/src/modules/GroupInfo.tsx
type GroupInfoProps (line 14) | interface GroupInfoProps {
function GroupInfo (line 25) | function GroupInfo(props: GroupInfoProps) {
FILE: packages/web/src/modules/InviteInfo.tsx
type GroupBasicInfo (line 14) | type GroupBasicInfo = {
function InviteInfo (line 20) | function InviteInfo() {
FILE: packages/web/src/modules/LoginAndRegister/Login.tsx
function Login (line 16) | function Login() {
FILE: packages/web/src/modules/LoginAndRegister/LoginAndRegister.tsx
function LoginAndRegister (line 17) | function LoginAndRegister() {
FILE: packages/web/src/modules/LoginAndRegister/Register.tsx
function Register (line 15) | function Register() {
FILE: packages/web/src/modules/Sidebar/About.tsx
type AboutProps (line 7) | interface AboutProps {
function About (line 12) | function About(props: AboutProps) {
FILE: packages/web/src/modules/Sidebar/Admin.tsx
type SystemConfig (line 30) | type SystemConfig = {
type AdminProps (line 35) | interface AdminProps {
function Admin (line 40) | function Admin(props: AdminProps) {
FILE: packages/web/src/modules/Sidebar/Download.tsx
type DownloadProps (line 8) | interface DownloadProps {
function Download (line 13) | function Download(props: DownloadProps) {
FILE: packages/web/src/modules/Sidebar/OnlineStatus.tsx
type OnlineStatusProps (line 5) | interface OnlineStatusProps {
function OnlineStatus (line 11) | function OnlineStatus(props: OnlineStatusProps) {
FILE: packages/web/src/modules/Sidebar/Reward.tsx
type RewardProps (line 8) | interface RewardProps {
function Reward (line 13) | function Reward(props: RewardProps) {
FILE: packages/web/src/modules/Sidebar/SelfInfo.tsx
type SelfInfoProps (line 22) | interface SelfInfoProps {
function SelfInfo (line 27) | function SelfInfo(props: SelfInfoProps) {
FILE: packages/web/src/modules/Sidebar/Setting.tsx
type SettingProps (line 29) | interface SettingProps {
type Color (line 34) | type Color = {
function Setting (line 42) | function Setting(props: SettingProps) {
FILE: packages/web/src/modules/Sidebar/Sidebar.tsx
function Sidebar (line 34) | function Sidebar() {
FILE: packages/web/src/modules/UserInfo.tsx
type UserInfoProps (line 22) | interface UserInfoProps {
function UserInfo (line 34) | function UserInfo(props: UserInfoProps) {
FILE: packages/web/src/service.ts
function saveUsername (line 4) | function saveUsername(username: string) {
function register (line 16) | async function register(
function login (line 47) | async function login(
function loginByToken (line 77) | async function loginByToken(
function guest (line 108) | async function guest(os = '', browser = '', environment = '') {
function changeAvatar (line 120) | async function changeAvatar(avatar: string) {
function changePassword (line 130) | async function changePassword(oldPassword: string, newPassword: string) {
function changeUsername (line 142) | async function changeUsername(username: string) {
function changeGroupName (line 154) | async function changeGroupName(groupId: string, name: string) {
function changeGroupAvatar (line 164) | async function changeGroupAvatar(groupId: string, avatar: string) {
function createGroup (line 173) | async function createGroup(name: string) {
function deleteGroup (line 182) | async function deleteGroup(groupId: string) {
function joinGroup (line 191) | async function joinGroup(groupId: string) {
function leaveGroup (line 200) | async function leaveGroup(groupId: string) {
function addFriend (line 209) | async function addFriend(userId: string) {
function deleteFriend (line 218) | async function deleteFriend(userId: string) {
function getLinkmansLastMessagesV2 (line 227) | async function getLinkmansLastMessagesV2(linkmanIds: string[]) {
function getLinkmanHistoryMessages (line 239) | async function getLinkmanHistoryMessages(
function getDefaultGroupHistoryMessages (line 254) | async function getDefaultGroupHistoryMessages(existCount: number) {
function search (line 265) | async function search(keywords: string) {
function searchExpression (line 274) | async function searchExpression(keywords: string) {
function sendMessage (line 285) | async function sendMessage(to: string, type: string, content: string) {
function deleteMessage (line 293) | async function deleteMessage(messageId: string) {
function getDefaultGroupOnlineMembers (line 338) | async function getDefaultGroupOnlineMembers() {
function sealUser (line 347) | async function sealUser(username: string) {
function sealIp (line 356) | async function sealIp(ip: string) {
function sealUserOnlineIp (line 365) | async function sealUserOnlineIp(userId: string) {
function getSealList (line 373) | async function getSealList() {
function getSystemConfig (line 378) | async function getSystemConfig() {
function resetUserPassword (line 387) | async function resetUserPassword(username: string) {
function setUserTag (line 397) | async function setUserTag(username: string, tag: string) {
function getUserIps (line 406) | async function getUserIps(userId: string) {
function getUserOnlineStatus (line 411) | async function getUserOnlineStatus(userId: string) {
function updateHistory (line 416) | async function updateHistory(linkmanId: string, messageId: string) {
function toggleSendMessage (line 421) | async function toggleSendMessage(enable: boolean) {
function toggleNewUserSendMessage (line 426) | async function toggleNewUserSendMessage(enable: boolean) {
FILE: packages/web/src/socket.ts
function loginFailback (line 34) | async function loginFailback() {
FILE: packages/web/src/state/action.ts
type ActionTypes (line 4) | enum ActionTypes {
type SetGuestPayload (line 43) | type SetGuestPayload = Group;
type SetUserPayload (line 45) | type SetUserPayload = {
type UpdateUserInfoPayload (line 55) | type UpdateUserInfoPayload = Object;
type SetStatusPayload (line 57) | interface SetStatusPayload {
type SetAvatarPayload (line 62) | type SetAvatarPayload = string;
type AddLinkmanPayload (line 64) | interface AddLinkmanPayload {
type SetFocusPayload (line 69) | type SetFocusPayload = string;
type SetLinkmansLastMessagesPayload (line 71) | interface SetLinkmansLastMessagesPayload {
type AddLinkmanHistoryMessagesPayload (line 78) | interface AddLinkmanHistoryMessagesPayload {
type AddLinkmanMessagePayload (line 83) | interface AddLinkmanMessagePayload {
type SetLinkmanPropertyPayload (line 88) | interface SetLinkmanPropertyPayload {
type RemoveLinkmanPayload (line 94) | type RemoveLinkmanPayload = string;
type UpdateMessagePayload (line 96) | interface UpdateMessagePayload {
type DeleteMessagePayload (line 102) | interface DeleteMessagePayload {
type Action (line 108) | interface Action {
FILE: packages/web/src/state/reducer.ts
type Message (line 21) | interface Message {
type MessagesMap (line 38) | interface MessagesMap {
type GroupMember (line 42) | interface GroupMember {
type Group (line 54) | interface Group {
type Friend (line 64) | interface Friend {
type Linkman (line 72) | interface Linkman extends Group, User {
type LinkmansMap (line 78) | interface LinkmansMap {
type User (line 83) | interface User {
type State (line 91) | interface State {
function getLinkmansMap (line 150) | function getLinkmansMap(linkmans: Linkman[]) {
function getMessagesMap (line 161) | function getMessagesMap(messages: Message[]) {
function deleteObjectKeys (line 173) | function deleteObjectKeys<T>(obj: T, keys: string[]): T {
function deleteObjectKey (line 190) | function deleteObjectKey<T>(obj: T, key: string): T {
function initLinkmanFields (line 199) | function initLinkmanFields(linkman: Linkman, type: string) {
function transformGroup (line 209) | function transformGroup(group: Linkman): Linkman {
function transformFriend (line 220) | function transformFriend(friend: Linkman): Linkman {
function transformTemporary (line 234) | function transformTemporary(temporary: Linkman): Linkman {
function reducer (line 265) | function reducer(state: State = initialState, action: Action): State {
FILE: packages/web/src/themes.ts
type Themes (line 4) | type Themes = {
FILE: packages/web/src/types/index.d.ts
type Window (line 51) | interface Window {
FILE: packages/web/src/utils/fetch.ts
function fetch (line 9) | function fetch<T = any>(
FILE: packages/web/src/utils/getRandomHuaji.ts
type Huaji (line 39) | type Huaji = {
function getRandomHuaji (line 84) | function getRandomHuaji() {
FILE: packages/web/src/utils/inobounce.ts
function inobounce (line 5) | function inobounce(targetElement: HTMLElement) {
FILE: packages/web/src/utils/notification.ts
function notification (line 1) | function notification(
FILE: packages/web/src/utils/playSound.ts
type Sounds (line 8) | type Sounds = {
function play (line 32) | async function play() {
function playSound (line 46) | function playSound(type = 'default') {
FILE: packages/web/src/utils/readDiskFile.ts
type ReadFileResult (line 1) | interface ReadFileResult {
function readDiskFIle (line 19) | async function readDiskFIle(
FILE: packages/web/src/utils/setCssVariable.ts
function setCssVariable (line 6) | function setCssVariable(color: string, textColor: string) {
FILE: packages/web/src/utils/uploadFile.ts
function initOSS (line 6) | async function initOSS() {
function getOSSFileUrl (line 38) | function getOSSFileUrl(url = '', process = '') {
function uploadFile (line 60) | async function uploadFile(
FILE: packages/web/src/utils/voice.ts
function read (line 13) | async function read(text: string, cuid: string) {
type Task (line 46) | type Task = {
function handleTaskQueue (line 53) | async function handleTaskQueue() {
method push (line 65) | push(text: string, cuid: string) {
Condensed preview — 319 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (734K chars).
[
{
"path": ".dockerignore",
"chars": 93,
"preview": "**/node_module\npackages/docs/\npackages/web/.linaria-cache/\npackages/web/dist/\n\nyarn-error.log"
},
{
"path": ".eslintignore",
"chars": 48,
"preview": "node_modules/\ndist/\npublic/\nbuild/\ndocs/\n*.d.ts\n"
},
{
"path": ".eslintrc",
"chars": 2138,
"preview": "{\n \"extends\": [\"eslint-config-airbnb\", \"prettier\"],\n \"parser\": \"@typescript-eslint/parser\",\n \"parserOptions\": {"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2368,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/lint.yml",
"chars": 564,
"preview": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versi"
},
{
"path": ".github/workflows/test.yml",
"chars": 301,
"preview": "name: Unit Test\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n test:\n runs-o"
},
{
"path": ".github/workflows/ts.yml",
"chars": 572,
"preview": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versi"
},
{
"path": ".gitignore",
"chars": 456,
"preview": ".DS_Store\nnode_modules/\ndist/\ncoverage/\n.idea/\n.linaria-cache/\n\nnpm-debug.log\nyarn-error.log\n.eslintcache\nlerna-debug.lo"
},
{
"path": ".prettierrc",
"chars": 125,
"preview": "{\n \"tabWidth\": 4,\n \"trailingComma\": \"all\",\n \"singleQuote\": true,\n \"arrowParens\": \"always\",\n \"printWidth\":"
},
{
"path": ".vscode/settings.json",
"chars": 56,
"preview": "{\n \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}"
},
{
"path": "Dockerfile",
"chars": 188,
"preview": "FROM node:14\n\nWORKDIR /usr/app/fiora\n\nCOPY packages ./packages\nCOPY package.json tsconfig.json yarn.lock lerna.json ./\nR"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2015-2021 碎碎酱\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 3576,
"preview": "# [Fiora](https://fiora.suisuijiang.com/) · []("
},
{
"path": "docker-compose.yaml",
"chars": 275,
"preview": "version: '3.2'\n\nservices:\n mongodb:\n image: mongo\n restart: always\n redis:\n image: redis\n restart: always\n"
},
{
"path": "index.ts",
"chars": 2429,
"preview": "#!/usr/bin/env ./node_modules/.bin/ts-node\n\nimport { program } from 'commander';\nimport cp from 'child_process';\nimport "
},
{
"path": "jest.config.js",
"chars": 485,
"preview": "module.exports = {\n preset: 'ts-jest',\n moduleNameMapper: {\n '^.+\\\\.(css|less|jpg|png|gif|mp3)$': '<rootDir"
},
{
"path": "jest.setup.js",
"chars": 185,
"preview": "jest.mock('./packages/web/node_modules/linaria', () => ({\n css: jest.fn(() => ''),\n}));\n\njest.mock('./packages/databa"
},
{
"path": "jest.transformer.js",
"chars": 168,
"preview": "const path = require('path');\n\nmodule.exports = {\n process(src, filename) {\n return `module.exports = ${JSON.s"
},
{
"path": "lerna.json",
"chars": 92,
"preview": "{\n \"packages\": [\n \"packages/*\"\n ],\n \"version\": \"independent\",\n \"npmClient\": \"yarn\"\n}\n"
},
{
"path": "package.json",
"chars": 1985,
"preview": "{\n \"name\": \"fiora\",\n \"version\": \"1.0.0\",\n \"description\": \"An interesting chat application power by socket.io, koa, mo"
},
{
"path": "packages/app/.babelrc",
"chars": 39,
"preview": "{\n \"presets\": [\"babel-preset-expo\"]\n}\n"
},
{
"path": "packages/app/.eslintrc",
"chars": 118,
"preview": "{\n \"extends\": \"../../.eslintrc\",\n \"rules\": {\n \"no-use-before-define\": \"off\",\n \"consistent-return\": \"off\"\n }\n}"
},
{
"path": "packages/app/.gitignore",
"chars": 262,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# expo\n.expo/\n.expo-shared/\n\n# dependencies\n"
},
{
"path": "packages/app/.watchmanconfig",
"chars": 3,
"preview": "{}\n"
},
{
"path": "packages/app/App.tsx",
"chars": 335,
"preview": "/* eslint-disable react/jsx-props-no-spreading */\nimport React from 'react';\nimport { Provider } from 'react-redux';\nimp"
},
{
"path": "packages/app/app.json",
"chars": 659,
"preview": "{\n \"expo\": {\n \"privacy\": \"public\",\n \"name\": \"fiora\",\n \"icon\": \"./icon.png\",\n \"version\": \"1.1.4\",\n \"descr"
},
{
"path": "packages/app/package.json",
"chars": 1691,
"preview": "{\n \"name\": \"@fiora/app\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"main\": \"./node_modules/expo/Ap"
},
{
"path": "packages/app/src/App.tsx",
"chars": 7115,
"preview": "import React from 'react';\nimport { StyleSheet, View } from 'react-native';\nimport { Scene, Router, Stack, Tabs, Lightbo"
},
{
"path": "packages/app/src/components/Avatar.tsx",
"chars": 525,
"preview": "import React from 'react';\nimport { getOSSFileUrl } from '../utils/uploadFile';\n\nimport Image from './Image';\n\ntype Prop"
},
{
"path": "packages/app/src/components/BackButton.tsx",
"chars": 903,
"preview": "import { View, Icon, Text } from 'native-base';\nimport React from 'react';\nimport { TouchableOpacity } from 'react-nativ"
},
{
"path": "packages/app/src/components/Expression.tsx",
"chars": 609,
"preview": "import React from 'react';\nimport { View } from 'react-native';\n\nimport Image from './Image';\nimport uri from '../assets"
},
{
"path": "packages/app/src/components/Image.tsx",
"chars": 1105,
"preview": "import React from 'react';\nimport { Image as BaseImage, ImageSourcePropType } from 'react-native';\nimport { getOSSFileUr"
},
{
"path": "packages/app/src/components/Loading.tsx",
"chars": 1115,
"preview": "import React from 'react';\nimport { View, Text, Dimensions, StyleSheet } from 'react-native';\nimport { Spinner } from 'n"
},
{
"path": "packages/app/src/components/Nofitication.tsx",
"chars": 3987,
"preview": "import Constants from 'expo-constants';\nimport * as Notifications from 'expo-notifications';\nimport { useState, useEffec"
},
{
"path": "packages/app/src/components/PageContainer.tsx",
"chars": 1098,
"preview": "import { View } from 'native-base';\nimport React from 'react';\nimport { ImageBackground, SafeAreaView, StyleSheet } from"
},
{
"path": "packages/app/src/components/Toast.tsx",
"chars": 524,
"preview": "import { Toast } from 'native-base';\n\nexport default {\n success(message: string) {\n Toast.show({\n t"
},
{
"path": "packages/app/src/hooks/useStore.tsx",
"chars": 1206,
"preview": "import { useSelector } from 'react-redux';\nimport { State, User } from '../types/redux';\n\nexport function useStore() {\n "
},
{
"path": "packages/app/src/pages/Chat/Chat.tsx",
"chars": 4795,
"preview": "import React, { useEffect, useRef } from 'react';\nimport {\n StyleSheet,\n KeyboardAvoidingView,\n ScrollView,\n "
},
{
"path": "packages/app/src/pages/Chat/ChatBackButton.tsx",
"chars": 408,
"preview": "import React from 'react';\nimport BackButton from '../../components/BackButton';\nimport { useStore } from '../../hooks/u"
},
{
"path": "packages/app/src/pages/Chat/ChatRightButton.tsx",
"chars": 1041,
"preview": "import { View, Icon } from 'native-base';\nimport React from 'react';\nimport { StyleSheet, TouchableOpacity } from 'react"
},
{
"path": "packages/app/src/pages/Chat/ImageMessage.tsx",
"chars": 1957,
"preview": "/* eslint-disable react/jsx-props-no-spreading */\nimport { View } from 'native-base';\nimport React from 'react';\nimport "
},
{
"path": "packages/app/src/pages/Chat/Input.tsx",
"chars": 11564,
"preview": "import React, { useRef, useState } from 'react';\nimport {\n StyleSheet,\n View,\n TextInput,\n Text,\n Dimensi"
},
{
"path": "packages/app/src/pages/Chat/InviteMessage.tsx",
"chars": 2278,
"preview": "import { View, Text } from 'native-base';\nimport React from 'react';\nimport { StyleSheet, TouchableNativeFeedback } from"
},
{
"path": "packages/app/src/pages/Chat/Message.tsx",
"chars": 9292,
"preview": "import React, { useEffect } from 'react';\nimport {\n View,\n Text,\n StyleSheet,\n Dimensions,\n TouchableOpac"
},
{
"path": "packages/app/src/pages/Chat/MessageList.tsx",
"chars": 6937,
"preview": "import React, { useEffect, useState } from 'react';\nimport { ScrollView, StyleSheet, Keyboard, Modal, Image } from 'reac"
},
{
"path": "packages/app/src/pages/Chat/SystemMessage.tsx",
"chars": 947,
"preview": "import { View, Text } from 'native-base';\nimport React from 'react';\nimport { StyleSheet } from 'react-native';\nimport {"
},
{
"path": "packages/app/src/pages/Chat/TextMessage.tsx",
"chars": 3430,
"preview": "import { View, Text } from 'native-base';\nimport React from 'react';\nimport { TouchableOpacity, Linking, StyleSheet } fr"
},
{
"path": "packages/app/src/pages/ChatList/ChatList.tsx",
"chars": 3199,
"preview": "import React, { useState } from 'react';\nimport { ScrollView, StyleSheet } from 'react-native';\n\nimport { Header, Item, "
},
{
"path": "packages/app/src/pages/ChatList/ChatListRightButton.tsx",
"chars": 2169,
"preview": "import { View, Icon } from 'native-base';\nimport React, { useState } from 'react';\nimport { StyleSheet, TouchableOpacity"
},
{
"path": "packages/app/src/pages/ChatList/Linkman.tsx",
"chars": 3434,
"preview": "import React from 'react';\nimport { Text, StyleSheet, View, TouchableOpacity } from 'react-native';\nimport { Actions } f"
},
{
"path": "packages/app/src/pages/ChatList/SelfInfo.tsx",
"chars": 1657,
"preview": "import { Text, View } from 'native-base';\nimport React from 'react';\nimport { StyleSheet } from 'react-native';\nimport A"
},
{
"path": "packages/app/src/pages/GroupInfo/GroupInfo.tsx",
"chars": 3819,
"preview": "import React from 'react';\nimport { Button, Text, View } from 'native-base';\nimport { StyleSheet } from 'react-native';\n"
},
{
"path": "packages/app/src/pages/GroupProfile/GroupProfile.tsx",
"chars": 3604,
"preview": "import { View, Text, Button } from 'native-base';\nimport React from 'react';\nimport { Alert, Pressable, ScrollView, Styl"
},
{
"path": "packages/app/src/pages/LoginSignup/Base.tsx",
"chars": 3225,
"preview": "import React, { useRef, useState } from 'react';\nimport { Alert, StyleSheet, Text, TextInput } from 'react-native';\nimpo"
},
{
"path": "packages/app/src/pages/LoginSignup/Login.tsx",
"chars": 1430,
"preview": "import React from 'react';\nimport { Container } from 'native-base';\nimport { Actions } from 'react-native-router-flux';\n"
},
{
"path": "packages/app/src/pages/LoginSignup/Signup.tsx",
"chars": 1554,
"preview": "import React from 'react';\nimport { Container, Toast } from 'native-base';\nimport { Actions } from 'react-native-router-"
},
{
"path": "packages/app/src/pages/Other/Other.tsx",
"chars": 6379,
"preview": "import {\n Body,\n Button,\n Content,\n Icon,\n List,\n ListItem,\n Right,\n Text,\n Toast,\n View,\n"
},
{
"path": "packages/app/src/pages/Other/PrivacyPolicy.tsx",
"chars": 1672,
"preview": "import { Text } from 'native-base';\nimport React from 'react';\nimport { Linking, StyleSheet, TouchableOpacity } from 're"
},
{
"path": "packages/app/src/pages/Other/Sponsor.tsx",
"chars": 1177,
"preview": "import { View, Text } from 'native-base';\nimport React from 'react';\nimport { StyleSheet } from 'react-native';\nimport D"
},
{
"path": "packages/app/src/pages/SearchResult/SearchResult.tsx",
"chars": 4126,
"preview": "import React from 'react';\nimport { Tab, Tabs, Text, View } from 'native-base';\nimport { ScrollView, StyleSheet, Touchab"
},
{
"path": "packages/app/src/pages/UserInfo/UserInfo.tsx",
"chars": 6456,
"preview": "import React from 'react';\nimport { Button, Text, View } from 'native-base';\nimport { StyleSheet } from 'react-native';\n"
},
{
"path": "packages/app/src/service.ts",
"chars": 7409,
"preview": "import { User } from './types/redux';\nimport fetch from './utils/fetch';\n\nfunction saveUsername(username: string) {\n "
},
{
"path": "packages/app/src/socket.ts",
"chars": 5093,
"preview": "import IO from 'socket.io-client';\nimport Toast from './components/Toast';\nimport action from './state/action';\nimport s"
},
{
"path": "packages/app/src/state/action.ts",
"chars": 6144,
"preview": "import getFriendId from '../utils/getFriendId';\nimport store from './store';\nimport {\n ConnectActionType,\n Connect"
},
{
"path": "packages/app/src/state/reducer.ts",
"chars": 9773,
"preview": "import produce from 'immer';\nimport deepmerge from 'deepmerge';\nimport {\n State,\n ActionTypes,\n ConnectActionTy"
},
{
"path": "packages/app/src/state/store.ts",
"chars": 262,
"preview": "import { createStore } from 'redux';\nimport reducer from './reducer';\n\nconst store = createStore(\n // @ts-ignore\n "
},
{
"path": "packages/app/src/types/global.d.ts",
"chars": 112,
"preview": "declare module '@react-native-toolkit/triangle';\ndeclare module 'react-native-dialog';\n\ndeclare module '*.png';\n"
},
{
"path": "packages/app/src/types/redux.ts",
"chars": 5413,
"preview": "export const ConnectActionType = 'SetConnect';\nexport type ConnectAction = {\n type: typeof ConnectActionType;\n val"
},
{
"path": "packages/app/src/types/socket.ts",
"chars": 95,
"preview": "export type Socket = {\n on: (event: string, callback: (...params: any) => void) => void;\n};\n"
},
{
"path": "packages/app/src/utils/constant.ts",
"chars": 57,
"preview": "export const referer = 'https://fiora.suisuijiang.com/';\n"
},
{
"path": "packages/app/src/utils/convertMessage.ts",
"chars": 2373,
"preview": "// function convertRobot10Message(message) {\n// if (message.from._id === '5adad39555703565e7903f79') {\n// tr"
},
{
"path": "packages/app/src/utils/expressions.ts",
"chars": 736,
"preview": "export default {\n default: [\n '呵呵',\n '哈哈',\n '吐舌',\n '啊',\n '酷',\n '怒',\n "
},
{
"path": "packages/app/src/utils/fetch.ts",
"chars": 569,
"preview": "import Toast from '../components/Toast';\nimport socket from '../socket';\n\nexport default function fetch<T = any>(\n ev"
},
{
"path": "packages/app/src/utils/getFriendId.ts",
"chars": 173,
"preview": "export default function getFriendId(userId1: string, userId2: string) {\n if (userId1 < userId2) {\n return user"
},
{
"path": "packages/app/src/utils/getRandomColor.ts",
"chars": 726,
"preview": "import randomColor from 'randomcolor';\n\ntype ColorMode = 'dark' | 'bright' | 'light' | 'random';\n\n/**\n * 获取随机颜色, 刷新页面不变\n"
},
{
"path": "packages/app/src/utils/linkman.ts",
"chars": 556,
"preview": "import { Friend, Group, Linkman } from '../types/redux';\n\nexport function formatLinkmanName(linkman: Linkman) {\n if ("
},
{
"path": "packages/app/src/utils/platform.ts",
"chars": 655,
"preview": "import { Platform } from 'react-native';\nimport Constants from 'expo-constants';\n// eslint-disable-next-line import/exte"
},
{
"path": "packages/app/src/utils/storage.ts",
"chars": 380,
"preview": "import AsyncStorage from '@react-native-async-storage/async-storage';\n\nexport async function getStorageValue(key: string"
},
{
"path": "packages/app/src/utils/time.ts",
"chars": 1161,
"preview": "export default {\n isToday(time1: Date, time2: Date) {\n return (\n time1.getFullYear() === time2.getF"
},
{
"path": "packages/app/src/utils/uploadFile.ts",
"chars": 1000,
"preview": "import fetch from './fetch';\n\n/**\n * 上传文件\n * @param blob 文件blob数据\n * @param fileName 文件名\n */\nexport default async functi"
},
{
"path": "packages/app/tests/state/reducer.test.ts",
"chars": 1436,
"preview": "import { mergeLinkmans } from '../../src/state/reducer';\nimport { Linkman } from '../../src/types/redux';\n\ndescribe('mer"
},
{
"path": "packages/app/tsconfig.json",
"chars": 35,
"preview": "{\n \"extends\": \"../../tsconfig\",\n}\n"
},
{
"path": "packages/assets/package.json",
"chars": 91,
"preview": "{\n \"name\": \"@fiora/assets\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true\n}\n"
},
{
"path": "packages/bin/index.ts",
"chars": 401,
"preview": "import chalk from 'chalk';\nimport path from 'path';\nimport fs from 'fs';\n\nconst script = process.argv[2];\nif (!script) {"
},
{
"path": "packages/bin/package.json",
"chars": 479,
"preview": "{\n \"name\": \"@fiora/bin\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"scripts\": {\n \"script\": \"ts-"
},
{
"path": "packages/bin/scripts/deleteMessages.ts",
"chars": 1904,
"preview": "import path from 'path';\nimport fs from 'fs';\nimport inquirer from 'inquirer';\nimport { promisify } from 'util';\nimport "
},
{
"path": "packages/bin/scripts/deleteTodayRegisteredUsers.ts",
"chars": 1329,
"preview": "/**\n * Delete users created today and their related data\n */\nimport chalk from 'chalk';\n\nimport inquirer from 'inquirer'"
},
{
"path": "packages/bin/scripts/deleteUser.ts",
"chars": 4075,
"preview": "/* eslint-disable no-console */\nimport chalk from 'chalk';\n\nimport inquirer from 'inquirer';\nimport User from '@fiora/da"
},
{
"path": "packages/bin/scripts/doctor.ts",
"chars": 1700,
"preview": "import chalk from 'chalk';\nimport cp from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport detect f"
},
{
"path": "packages/bin/scripts/fixUsersAvatar.ts",
"chars": 1542,
"preview": "import chalk from 'chalk';\nimport inquirer from 'inquirer';\nimport User from '@fiora/database/mongoose/models/user';\nimp"
},
{
"path": "packages/bin/scripts/getUserId.ts",
"chars": 763,
"preview": "import chalk from 'chalk';\nimport User from '@fiora/database/mongoose/models/user';\nimport initMongoDB from '@fiora/data"
},
{
"path": "packages/bin/scripts/register.ts",
"chars": 2176,
"preview": "/**\n * Register\n */\n\nimport bcrypt from 'bcryptjs';\nimport chalk from 'chalk';\n\nimport initMongoDB from '@fiora/database"
},
{
"path": "packages/bin/scripts/updateDefaultGroupName.ts",
"chars": 983,
"preview": "import chalk from 'chalk';\nimport initMongoDB from '@fiora/database/mongoose/initMongoDB';\nimport Group from '@fiora/dat"
},
{
"path": "packages/bin/tsconfig.json",
"chars": 34,
"preview": "{\n \"extends\": \"../../tsconfig\",\n}"
},
{
"path": "packages/config/client.ts",
"chars": 1185,
"preview": "import { MB } from '../utils/const';\n\nexport default {\n server:\n process.env.Server ||\n (process.env.NO"
},
{
"path": "packages/config/package.json",
"chars": 189,
"preview": "{\n \"name\": \"@fiora/config\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"dependencies\": {\n \"ip\": "
},
{
"path": "packages/config/server.ts",
"chars": 1580,
"preview": "import ip from 'ip';\n\nconst { env } = process;\n\nexport default {\n /** 服务端host, 默认为本机ip地址(可能会是局域网地址) */\n host: env."
},
{
"path": "packages/database/mongoose/index.ts",
"chars": 26,
"preview": "export * from 'mongoose';\n"
},
{
"path": "packages/database/mongoose/initMongoDB.ts",
"chars": 697,
"preview": "/**\n * 连接 MongoDB\n */\n\nimport mongoose from 'mongoose';\n\nimport config from '@fiora/config/server';\nimport logger from '"
},
{
"path": "packages/database/mongoose/models/friend.ts",
"chars": 608,
"preview": "import { Schema, model, Document } from 'mongoose';\n\nconst FriendSchema = new Schema({\n createTime: { type: Date, def"
},
{
"path": "packages/database/mongoose/models/group.ts",
"chars": 1101,
"preview": "import { Schema, model, Document } from 'mongoose';\nimport { NAME_REGEXP } from '@fiora/utils/const';\n\nconst GroupSchema"
},
{
"path": "packages/database/mongoose/models/history.ts",
"chars": 1029,
"preview": "import { Schema, model, Document } from 'mongoose';\n\nconst HistoryScheme = new Schema({\n user: {\n type: String"
},
{
"path": "packages/database/mongoose/models/message.ts",
"chars": 2236,
"preview": "import { Schema, model, Document } from 'mongoose';\nimport Group from './group';\nimport User from './user';\n\nconst Messa"
},
{
"path": "packages/database/mongoose/models/notification.ts",
"chars": 512,
"preview": "import { Schema, model, Document } from 'mongoose';\n\nconst NotificationSchema = new Schema({\n createTime: { type: Dat"
},
{
"path": "packages/database/mongoose/models/socket.ts",
"chars": 944,
"preview": "import { Schema, model, Document } from 'mongoose';\n\nconst SocketSchema = new Schema({\n createTime: { type: Date, def"
},
{
"path": "packages/database/mongoose/models/user.ts",
"chars": 1152,
"preview": "import { Schema, model, Document } from 'mongoose';\nimport { NAME_REGEXP } from '@fiora/utils/const';\n\nconst UserSchema "
},
{
"path": "packages/database/package.json",
"chars": 321,
"preview": "{\n \"name\": \"@fiora/database\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"dependencies\": {\n \"@fi"
},
{
"path": "packages/database/redis/initRedis.ts",
"chars": 1951,
"preview": "import redis from 'redis';\nimport { promisify } from 'util';\nimport config from '@fiora/config/server';\nimport logger fr"
},
{
"path": "packages/database/tsconfig.json",
"chars": 34,
"preview": "{\n \"extends\": \"../../tsconfig\",\n}"
},
{
"path": "packages/docs/.gitignore",
"chars": 233,
"preview": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.lo"
},
{
"path": "packages/docs/babel.config.js",
"chars": 89,
"preview": "module.exports = {\n presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
},
{
"path": "packages/docs/docs/API.md",
"chars": 16,
"preview": "---\nid: api\n---\n"
},
{
"path": "packages/docs/docs/App.md",
"chars": 16,
"preview": "---\nid: app\n---\n"
},
{
"path": "packages/docs/docs/CHANGELOG.md",
"chars": 22,
"preview": "---\nid: changelog\n---\n"
},
{
"path": "packages/docs/docs/Config.md",
"chars": 19,
"preview": "---\nid: config\n---\n"
},
{
"path": "packages/docs/docs/FAQ.md",
"chars": 16,
"preview": "---\nid: faq\n---\n"
},
{
"path": "packages/docs/docs/Getting-Start.md",
"chars": 26,
"preview": "---\nid: getting-start\n---\n"
},
{
"path": "packages/docs/docs/INSTALL.md",
"chars": 20,
"preview": "---\nid: install\n---\n"
},
{
"path": "packages/docs/docs/Script.md",
"chars": 19,
"preview": "---\nid: script\n---\n"
},
{
"path": "packages/docs/docusaurus.config.js",
"chars": 3669,
"preview": "module.exports = {\n title: 'fiora docs',\n tagline: 'An interesting open source chat application',\n url: 'https:"
},
{
"path": "packages/docs/i18n/en/code.json",
"chars": 1981,
"preview": "{\n \"Title\": {\n \"message\": \"fiora\"\n },\n \"TagLine\": {\n \"message\": \"An interesting open source chat "
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/API.md",
"chars": 6041,
"preview": "---\nid: api\ntitle: API\nsidebar_label: API\n---\n\n## 如何调用接口\n\nfiora 后端基于 socket.io, 首先需要与后端建立连接\n\n```js\nimport IO from 'socke"
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/App.md",
"chars": 995,
"preview": "---\nid: app\ntitle: Fiora App\nsidebar_label: Fiora App\n---\n\nFiora app is developed with [expo](https://expo.io/) and [rea"
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/CHANGELOG.md",
"chars": 2335,
"preview": "---\nid: changelog\ntitle: Change Log\nsidebar_label: Change Log\n---\n\n## 2021-6-24\n\n- Support user names and user tags wi"
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/Config.md",
"chars": 5849,
"preview": "---\nid: config\ntitle: Config\nsidebar_label: Config\n---\n\nServer configuration `config/server.ts`\nClient configuration `co"
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/FAQ.md",
"chars": 3351,
"preview": "---\nid: faq\ntitle: FAQ\nsidebar_label: FAQ\n---\n\n## How to set up an administrator\n\n1. Get user id. reference [getUserId]("
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/Getting-Start.md",
"chars": 3159,
"preview": "---\nid: getting-start\ntitle: Getting Start\nsidebar_label: Getting Start\n---\n\nimport useBaseUrl from '@docusaurus/useBase"
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/INSTALL.md",
"chars": 2560,
"preview": "---\nid: install\ntitle: Install\nsidebar_label: Install\n---\n\n## Environmental Preparation\n\nTo run Fiora, you need Node.js("
},
{
"path": "packages/docs/i18n/en/docusaurus-plugin-content-docs/current/Script.md",
"chars": 1433,
"preview": "---\nid: script\ntitle: Script\nsidebar_label: Script\n---\n\nFiora has a built-in command line tool to manage the server. Exe"
},
{
"path": "packages/docs/i18n/en/docusaurus-theme-classic/footer.json",
"chars": 545,
"preview": "{\n \"link.title.Docs\": {\n \"message\": \"Docs\"\n },\n \"link.item.label.Overview\": {\n \"message\": \"Overvi"
},
{
"path": "packages/docs/i18n/en/docusaurus-theme-classic/navbar.json",
"chars": 61,
"preview": "{\n \"item.label.Docs\": {\n \"message\": \"Docs\"\n }\n}\n"
},
{
"path": "packages/docs/i18n/zh-Hans/code.json",
"chars": 1401,
"preview": "{\n \"Title\": {\n \"message\": \"fiora\"\n },\n \"TagLine\": {\n \"message\": \"一个有趣的开源聊天应用\"\n },\n \"Keyword"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/API.md",
"chars": 6039,
"preview": "---\nid: api\ntitle: 接口\nsidebar_label: 接口\n---\n\n## 如何调用接口\n\nfiora 后端基于 socket.io, 首先需要与后端建立连接\n\n```js\nimport IO from 'socket."
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/App.md",
"chars": 708,
"preview": "---\nid: app\ntitle: Fiora App\nsidebar_label: Fiora App\n---\n\nfiora app 是基于 [expo](https://expo.io/) he [react-native](htt"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/CHANGELOG.md",
"chars": 1181,
"preview": "---\nid: changelog\ntitle: 更新日志\nsidebar_label: 更新日志\n---\n\n## 2021-6-24\n\n- 支持带有日文字符的用户名和用户标签\n\n## 2021-5-11\n\n- 使用阿里云 OSS "
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/Config.md",
"chars": 4567,
"preview": "---\nid: config\ntitle: 配置\nsidebar_label: 配置\n---\n\n服务器配置 `config/server.ts`\n客户端配置 `config/client.ts`\n\n相比于直接修改配置文件, 推荐用环境变量来"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/FAQ.md",
"chars": 2314,
"preview": "---\nid: faq\ntitle: 问题解答\nsidebar_label: 问题解答\n---\n\n## 如何设置管理员\n\n1. 获取用户 id, 参考 [getUserId](/docs/script#getuserid)\n2. 修改 `A"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/Getting-Start.md",
"chars": 2277,
"preview": "---\nid: getting-start\ntitle: 入门指南\nsidebar_label: 入门指南\n---\n\nimport useBaseUrl from '@docusaurus/useBaseUrl';\n\nfiora 是一款有趣"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/INSTALL.md",
"chars": 1953,
"preview": "---\nid: install\ntitle: 安装\nsidebar_label: 安装\n---\n\n## 环境准备\n\n要运行 Fiora, 你需要 Node.js(推荐 v14 LTS 版本), MongoDB 和 redis\n\n- 安装"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/Script.md",
"chars": 754,
"preview": "---\nid: script\ntitle: 脚本\nsidebar_label: 脚本\n---\n\nfiora 内置了一个命令行工具, 用来管理服务器. 执行 `fiora` 查看工具\n\n> **注意!** 这些脚本大多会直接修改数据库, 推"
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-theme-classic/footer.json",
"chars": 651,
"preview": "{\n \"link.title.Docs\": {\n \"message\": \"文档\"\n },\n \"link.item.label.Overview\": {\n \"message\": \"首页\"\n "
},
{
"path": "packages/docs/i18n/zh-Hans/docusaurus-theme-classic/navbar.json",
"chars": 59,
"preview": "{\n \"item.label.Docs\": {\n \"message\": \"文档\"\n }\n}\n"
},
{
"path": "packages/docs/package.json",
"chars": 851,
"preview": "{\n \"name\": \"@fiora/docs\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"docusaurus\": \"docusaurus\",\n \""
},
{
"path": "packages/docs/sidebars.js",
"chars": 156,
"preview": "module.exports = {\n docs: {\n fiora: ['getting-start', 'install', 'config', 'script', 'faq', 'changelog'],\n "
},
{
"path": "packages/docs/src/css/custom.css",
"chars": 836,
"preview": "/* stylelint-disable docusaurus/copyright-header */\n/**\n * Any CSS included here will be global. The classic template\n *"
},
{
"path": "packages/docs/src/pages/index.js",
"chars": 6783,
"preview": "import React from 'react';\nimport clsx from 'clsx';\nimport Layout from '@theme/Layout';\nimport Link from '@docusaurus/Li"
},
{
"path": "packages/docs/src/pages/styles.module.css",
"chars": 1510,
"preview": "/* stylelint-disable docusaurus/copyright-header */\n\n/**\n * CSS files with the .module.css suffix will be treated as CSS"
},
{
"path": "packages/docs/static/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "packages/i18n/en-US/bin.ts",
"chars": 604,
"preview": "export const getUserIdDescription = 'Get user id by username';\n\nexport const registerDescription = 'Register a new user'"
},
{
"path": "packages/i18n/en-US/index.ts",
"chars": 23,
"preview": "export * from './bin';\n"
},
{
"path": "packages/i18n/node.index.ts",
"chars": 358,
"preview": "import osLocale from 'os-locale';\n\nimport * as zhCN from './zh-CN';\nimport * as enUS from './en-US';\n\nconst languages = "
},
{
"path": "packages/i18n/package.json",
"chars": 140,
"preview": "{\n \"name\": \"@fiora/i18n\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"dependencies\": {\n \"os-loca"
},
{
"path": "packages/i18n/zh-CN/bin.ts",
"chars": 438,
"preview": "export const getUserIdDescription = '通过用户名获取 user id';\n\nexport const registerDescription = '注册新用户';\n\nexport const delete"
},
{
"path": "packages/i18n/zh-CN/index.ts",
"chars": 23,
"preview": "export * from './bin';\n"
},
{
"path": "packages/server/.nodemonrc",
"chars": 89,
"preview": "{\n \"verbose\": true,\n \"ignore\": [\n \"test/**/*\",\n \"public/**/*\"\n ]\n}"
},
{
"path": "packages/server/package.json",
"chars": 1258,
"preview": "{\n \"name\": \"@fiora/server\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"scripts\": {\n \"start\": \"c"
},
{
"path": "packages/server/public/PrivacyPolicy.html",
"chars": 10072,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <link\n href=\"https://fonts.googleapis.com/css?family=Roboto:100,300"
},
{
"path": "packages/server/public/index.html",
"chars": 296,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width"
},
{
"path": "packages/server/public/manifest.json",
"chars": 627,
"preview": "{\n \"name\": \"fiora\",\n \"short_name\": \"fiora\",\n \"start_url\": \"/\",\n \"display\": \"standalone\",\n \"background_col"
},
{
"path": "packages/server/src/app.ts",
"chars": 2677,
"preview": "import Koa from 'koa';\nimport koaSend from 'koa-send';\nimport koaStatic from 'koa-static';\nimport path from 'path';\nimpo"
},
{
"path": "packages/server/src/main.ts",
"chars": 1200,
"preview": "import config from '@fiora/config/server';\nimport getRandomAvatar from '@fiora/utils/getRandomAvatar';\nimport { doctor }"
},
{
"path": "packages/server/src/middlewares/frequency.ts",
"chars": 2173,
"preview": "import { Socket } from 'socket.io';\nimport {\n getNewUserKey,\n getSealUserKey,\n Redis,\n} from '@fiora/database/r"
},
{
"path": "packages/server/src/middlewares/isAdmin.ts",
"chars": 924,
"preview": "import config from '@fiora/config/server';\nimport { Socket } from 'socket.io';\n\nexport const YOU_ARE_NOT_ADMINISTRATOR ="
},
{
"path": "packages/server/src/middlewares/isLogin.ts",
"chars": 667,
"preview": "import { Socket } from 'socket.io';\n\nexport const PLEASE_LOGIN = '请登录后再试';\n\n/**\n * 拦截未登录用户请求需要登录态的接口\n */\nexport default "
},
{
"path": "packages/server/src/middlewares/registerRoutes.ts",
"chars": 2164,
"preview": "import assert from 'assert';\nimport logger from '@fiora/utils/logger';\nimport { getSocketIp } from '@fiora/utils/socket'"
},
{
"path": "packages/server/src/middlewares/seal.ts",
"chars": 722,
"preview": "import { SEAL_TEXT } from '@fiora/utils/const';\nimport { getSocketIp } from '@fiora/utils/socket';\nimport { Socket } fro"
},
{
"path": "packages/server/src/routes/group.ts",
"chars": 8958,
"preview": "import assert, { AssertionError } from 'assert';\nimport { Types } from '@fiora/database/mongoose';\nimport stringHash fro"
},
{
"path": "packages/server/src/routes/history.ts",
"chars": 1220,
"preview": "import { isValidObjectId, Types } from '@fiora/database/mongoose';\nimport assert from 'assert';\nimport User from '@fiora"
},
{
"path": "packages/server/src/routes/message.ts",
"chars": 14643,
"preview": "/* eslint-disable no-await-in-loop */\n/* eslint-disable no-restricted-syntax */\nimport assert, { AssertionError } from '"
},
{
"path": "packages/server/src/routes/notification.ts",
"chars": 950,
"preview": "import { AssertionError } from 'assert';\nimport User from '@fiora/database/mongoose/models/user';\nimport Notification fr"
},
{
"path": "packages/server/src/routes/system.ts",
"chars": 9955,
"preview": "import fs from 'fs';\nimport path from 'path';\nimport axios from 'axios';\nimport assert, { AssertionError } from 'assert'"
},
{
"path": "packages/server/src/routes/user.ts",
"chars": 14992,
"preview": "import bcrypt from 'bcryptjs';\nimport assert, { AssertionError } from 'assert';\nimport jwt from 'jwt-simple';\nimport { T"
},
{
"path": "packages/server/src/types/index.d.ts",
"chars": 31,
"preview": "declare module 'regex-escape';\n"
},
{
"path": "packages/server/src/types/server.d.ts",
"chars": 672,
"preview": "declare interface Context<T> {\n data: T;\n socket: {\n id: string;\n ip: string;\n user: string;\n"
},
{
"path": "packages/server/test/helpers/middleware.ts",
"chars": 202,
"preview": "export function getMiddlewareParams(event = 'login', data = {}) {\n const cb = jest.fn();\n const next = jest.fn();\n"
},
{
"path": "packages/server/test/middlewares/frequency.spec.ts",
"chars": 2683,
"preview": "import { mocked } from 'ts-jest/utils';\nimport { Redis } from '@fiora/database/redis/initRedis';\nimport { Socket } from "
},
{
"path": "packages/server/test/middlewares/isAdmin.spec.ts",
"chars": 1260,
"preview": "import { mocked } from 'ts-jest/utils';\nimport config from '@fiora/config/server';\nimport { Socket } from 'socket.io';\ni"
},
{
"path": "packages/server/test/middlewares/isLogin.spec.ts",
"chars": 1391,
"preview": "import { Socket } from 'socket.io';\nimport isLogin, { PLEASE_LOGIN } from '../../src/middlewares/isLogin';\nimport { getM"
},
{
"path": "packages/server/test/middlewares/seal.spec.ts",
"chars": 1506,
"preview": "import { mocked } from 'ts-jest/utils';\nimport { SEAL_TEXT } from '@fiora/utils/const';\nimport { Socket } from 'socket.i"
},
{
"path": "packages/server/tsconfig.json",
"chars": 34,
"preview": "{\n \"extends\": \"../../tsconfig\",\n}"
},
{
"path": "packages/utils/compressImage.ts",
"chars": 612,
"preview": "/**\n * 压缩图片\n * @param image 要压缩的图片\n * @param mimeType mime类型\n * @param quality 质量\n */\nexport default function compressIm"
},
{
"path": "packages/utils/const.ts",
"chars": 552,
"preview": "/** 封禁后提示文案 */\nexport const SEAL_TEXT = '你已经被关进小黑屋中, 请反思后再试';\n\n/** 封禁用户释放时间 */\nexport const SEAL_USER_TIMEOUT = 1000 * 6"
},
{
"path": "packages/utils/convertMessage.ts",
"chars": 1837,
"preview": "import WuZeiNiangImage from '@fiora/assets/images/wuzeiniang.gif';\n\n// function convertRobot10Message(message) {\n// "
},
{
"path": "packages/utils/expressions.ts",
"chars": 736,
"preview": "export default {\n default: [\n '呵呵',\n '哈哈',\n '吐舌',\n '啊',\n '酷',\n '怒',\n "
},
{
"path": "packages/utils/getFriendId.ts",
"chars": 334,
"preview": "/**\n * Combina two users id as frind id\n * The result has nothing to do with the order of the parameters\n * @param userI"
},
{
"path": "packages/utils/getRandomAvatar.ts",
"chars": 345,
"preview": "const AvatarCount = 15;\nconst publicPath = process.env.PublicPath || '/';\n\n/**\n * 获取随机头像\n */\nexport default function get"
},
{
"path": "packages/utils/getRandomColor.ts",
"chars": 726,
"preview": "import randomColor from 'randomcolor';\n\ntype ColorMode = 'dark' | 'bright' | 'light' | 'random';\n\n/**\n * 获取随机颜色, 刷新页面不变\n"
},
{
"path": "packages/utils/logger.ts",
"chars": 163,
"preview": "import { getLogger } from 'log4js';\n\nconst logger = getLogger();\nlogger.level = process.env.NODE_ENV === 'development' ?"
},
{
"path": "packages/utils/package.json",
"chars": 359,
"preview": "{\n \"name\": \"@fiora/utils\",\n \"version\": \"1.0.0\",\n \"license\": \"MIT\",\n \"private\": true,\n \"dependencies\": {\n \"@fiora"
},
{
"path": "packages/utils/sleep.ts",
"chars": 135,
"preview": "export default function sleep(duration = 200) {\n return new Promise((resolve) => {\n setTimeout(resolve, durati"
},
{
"path": "packages/utils/socket.ts",
"chars": 228,
"preview": "import { Socket } from 'socket.io';\n\nexport function getSocketIp(socket: Socket) {\n return (\n (socket.handshak"
},
{
"path": "packages/utils/test/getFriendId.spec.ts",
"chars": 329,
"preview": "import getFriendId from '../getFriendId';\n\ndescribe('utils/getFriendId.ts', () => {\n it('should combina two users id "
},
{
"path": "packages/utils/test/url.spec.ts",
"chars": 678,
"preview": "import { addParam } from '../url';\n\ndescribe('utils/url.ts', () => {\n it('should add ?key=value into url', () => {\n "
},
{
"path": "packages/utils/time.ts",
"chars": 928,
"preview": "export default {\n isToday(time1: Date, time2: Date) {\n return (\n time1.getFullYear() === time2.getF"
},
{
"path": "packages/utils/ua.ts",
"chars": 173,
"preview": "const UA = window.navigator.userAgent;\n\nexport const isiOS = /iPhone/i.test(UA);\n\nexport const isAndroid = /android/i.te"
},
{
"path": "packages/utils/url.ts",
"chars": 427,
"preview": "interface UrlParams {\n [key: string]: string;\n}\n\n// eslint-disable-next-line import/prefer-default-export\nexport func"
},
{
"path": "packages/utils/xss.ts",
"chars": 138,
"preview": "import xss from 'xss';\n\n/**\n * xss防护\n * @param text 要处理的文字\n */\nexport default function processXss(text: string) {\n re"
},
{
"path": "packages/web/.babelrc",
"chars": 1076,
"preview": "{\n \"presets\": [\n [\n \"@babel/preset-env\",\n {\n \"targets\": \"> 0.25%, not dea"
},
{
"path": "packages/web/build/webpack.common.js",
"chars": 4200,
"preview": "const path = require('path');\nconst webpack = require('webpack');\nconst { CleanWebpackPlugin } = require('clean-webpack-"
},
{
"path": "packages/web/build/webpack.dev.js",
"chars": 922,
"preview": "const path = require('path');\nconst { merge } = require('webpack-merge');\nconst ReactRefreshWebpackPlugin = require('@pm"
}
]
// ... and 119 more files (download for full content)
About this extraction
This page contains the full source code of the yinxin630/fiora GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 319 files (669.4 KB), approximately 167.4k tokens, and a symbol index with 499 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.