Update packages to @status-im (#223)
* Downgrade to yarn v1 * Rename status-communities to status-core * Rename chat-sdk to status-react * Rename packages in examples * Update readme * Remove changelog * Add extensions recommendations * Update gitignore * Rename package imports
This commit is contained in:
commit
f1b125cc4d
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": "./tsconfig.json" },
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"mocha": true,
|
||||
"browser": true
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "build", "coverage", "proto"],
|
||||
"plugins": ["import", "eslint-comments", "functional"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": ["error"],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"eslint-comments/disable-enable-pair": [
|
||||
"error",
|
||||
{ "allowWholeFile": true }
|
||||
],
|
||||
"eslint-comments/no-unused-disable": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
|
||||
],
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true }
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.spec.ts", "**/test_utils/*.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node 16
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: DEBUG=communities:test* yarn test
|
|
@ -0,0 +1,74 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.DS_Store
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Build output
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Status Research & Development GmbH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Status Research & Development GmbH
|
||||
|
||||
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.
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"fix": "run-s 'fix:*' && wsrun -e -c -s fix",
|
||||
"fix:prettier": "prettier \"./*.json\" --write",
|
||||
"build": "wsrun -e -c -s build",
|
||||
"test": "wsrun -e -c -s test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.2",
|
||||
"wsrun": "^5.2.4"
|
||||
},
|
||||
"packageManager": "yarn@1.22.17"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@waku/preview-proxy",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn node src/index.js",
|
||||
"fix": "",
|
||||
"build": "",
|
||||
"test": ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import fetch from 'node-fetch'
|
||||
import https from 'https'
|
||||
import fs from 'fs'
|
||||
|
||||
const regEx = new RegExp(/meta +(property|content)="(.+?)" +(property|content)="(.+?)"/g);
|
||||
|
||||
async function listener(req, res){
|
||||
const origin = req?.headers?.origin
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
if (origin === 'https://0.0.0.0:8080' || origin === 'https://localhost:8080' || origin === 'https://127.0.0.1:8080') {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
|
||||
const requestBody = await new Promise((resolve) => {
|
||||
if (req.method == 'POST') {
|
||||
let body = '';
|
||||
req.on('data', function (data) {
|
||||
body += data;
|
||||
if (body.length > 1e6)
|
||||
req.connection.destroy();
|
||||
});
|
||||
req.on('end', function () {
|
||||
try {
|
||||
resolve(JSON.parse(body))
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve({})
|
||||
}
|
||||
})
|
||||
const obj = {}
|
||||
if ('site' in requestBody) {
|
||||
try {
|
||||
const response = await fetch(requestBody['site'])
|
||||
const body = await response.text()
|
||||
for (const match of body.matchAll(regEx)) {
|
||||
if (match[1] === 'property') {
|
||||
obj[match[2]] = match[4]
|
||||
} else {
|
||||
obj[match[4]] = match[2]
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
res.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
const options = {
|
||||
key: fs.readFileSync('../../../cert/CA/localhost/localhost.decrypted.key'),
|
||||
cert: fs.readFileSync('../../../cert/CA/localhost/localhost.crt')
|
||||
}
|
||||
|
||||
const server = https.createServer(options, listener);
|
||||
server.listen(3000, () => console.log('server running at port 3000'));
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": "./tsconfig.json" },
|
||||
"env": { "es6": true },
|
||||
"ignorePatterns": ["node_modules", "dist", "coverage", "proto"],
|
||||
"plugins": ["import", "eslint-comments", "functional"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier"
|
||||
],
|
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"eslint-comments/disable-enable-pair": [
|
||||
"error",
|
||||
{ "allowWholeFile": true }
|
||||
],
|
||||
"eslint-comments/no-unused-disable": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
|
||||
],
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true }
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.spec.ts", "**/test_utils/*.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/*.spec.ts",
|
||||
"require": "ts-node/register",
|
||||
"exit": true
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# package.json is formatted by package managers, so we ignore it here
|
||||
package.json
|
|
@ -0,0 +1,13 @@
|
|||
#React chat example
|
||||
|
||||
##How to run example
|
||||
|
||||
1. First you need to `yarn && yarn build` in main repo folder
|
||||
2. set two environment libraries
|
||||
ENV and COMMUNITY_KEY
|
||||
|
||||
`export ENV=test` to use waku test fleet
|
||||
`export ENV=prod` to use waku prod fleet
|
||||
`export COMMUNITY_KEY=0x038ff8c6539ff268e024d07534a362ef69f7b13b056fcf19177fb6282b4d547bc8` to set a key to community
|
||||
|
||||
3. run `yarn start` in `packages/react-chat-example` folder.
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"name": "@waku/react-chat-sdk-example",
|
||||
"main": "index.js",
|
||||
"version": "0.1.0",
|
||||
"repository": "https://github.com/status-im/wakuconnect-chat-sdk/",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"packageManager": "yarn@3.0.1",
|
||||
"scripts": {
|
||||
"clean:all": "yarn clean && rimraf node_modules/",
|
||||
"clean": "rimraf dist/",
|
||||
"build": "rm -rf dist && webpack --mode=production --env ENV=production",
|
||||
"start": "webpack serve --mode=development --env ENV=$ENV COMMUNITY_KEY=$COMMUNITY_KEY --https",
|
||||
"fix": "run-s 'fix:*'",
|
||||
"fix:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --write",
|
||||
"fix:lint": "eslint './{src,test}/**/*.{ts,tsx}' --fix",
|
||||
"test": "run-s 'test:*'",
|
||||
"test:lint": "eslint './{src,test}/**/*.{ts,tsx}'",
|
||||
"test:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --list-different"
|
||||
},
|
||||
"dependencies": {
|
||||
"@status-im/react": "^0.0.0",
|
||||
"assert": "^2.0.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"process": "^0.11.10",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"styled-components": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-hooks": "^7.0.1",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.4.12",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router": "^5.1.16",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/styled-components": "^5.1.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"chai": "^4.3.4",
|
||||
"css-loader": "^6.3.0",
|
||||
"esbuild-loader": "^2.15.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-hooks": "^0.2.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.1",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jsdom": "^16.7.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"mocha": "^9.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"style-loader": "^3.3.0",
|
||||
"ts-loader": "^9.2.5",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.48.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/* /index.html 200
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>Waku Connect Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script type="module" src="/index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,136 @@
|
|||
import { CommunityChat, darkTheme, lightTheme } from "@waku/react-chat-sdk";
|
||||
import React, { useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import styled from "styled-components";
|
||||
|
||||
const fetchMetadata = async (link: string) => {
|
||||
const response = await fetch("https://localhost:3000", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ site: link }),
|
||||
});
|
||||
const body = await response.text();
|
||||
const parsedBody = JSON.parse(body);
|
||||
if (
|
||||
"og:image" in parsedBody &&
|
||||
"og:site_name" in parsedBody &&
|
||||
"og:title" in parsedBody
|
||||
) {
|
||||
return JSON.parse(body);
|
||||
}
|
||||
};
|
||||
|
||||
function DragDiv() {
|
||||
const [x, setX] = useState(0);
|
||||
const [y, setY] = useState(0);
|
||||
const [width, setWidth] = useState(window.innerWidth - 50);
|
||||
const [height, setHeight] = useState(window.innerHeight - 50);
|
||||
const [showChat, setShowChat] = useState(true);
|
||||
const ref = useRef<HTMLHeadingElement>(null);
|
||||
const moved = useRef(false);
|
||||
const setting = useRef("");
|
||||
|
||||
const [theme, setTheme] = useState(true);
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (setting.current === "position") {
|
||||
e.preventDefault();
|
||||
setX(e.x - 20);
|
||||
setY(e.y - 20);
|
||||
}
|
||||
if (setting.current === "size") {
|
||||
setWidth(e.x - x);
|
||||
setHeight(e.y - y);
|
||||
e.preventDefault();
|
||||
}
|
||||
moved.current = true;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
if (!moved.current) [setShowChat((prev) => !prev)];
|
||||
moved.current = false;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setTheme(!theme);
|
||||
}}
|
||||
>
|
||||
Change theme
|
||||
</button>
|
||||
<Drag style={{ left: x, top: y, width: width, height: height }} ref={ref}>
|
||||
<Bubble
|
||||
onMouseDown={() => {
|
||||
setting.current = "position";
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
/>
|
||||
<FloatingDiv className={showChat ? "" : "hide"}>
|
||||
<CommunityChat
|
||||
theme={theme ? lightTheme : darkTheme}
|
||||
communityKey={process.env.COMMUNITY_KEY ?? ""}
|
||||
config={{
|
||||
environment: process.env.ENV ?? "",
|
||||
dappUrl: "https://0.0.0.0:8080",
|
||||
}}
|
||||
fetchMetadata={fetchMetadata}
|
||||
/>
|
||||
</FloatingDiv>
|
||||
{showChat && (
|
||||
<SizeSet
|
||||
onMouseDown={() => {
|
||||
setting.current = "size";
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
></SizeSet>
|
||||
)}
|
||||
</Drag>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FloatingDiv = styled.div`
|
||||
height: calc(100% - 50px);
|
||||
border: 1px solid black;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SizeSet = styled.div`
|
||||
margin-left: auto;
|
||||
margin-right: 0px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: light-grey;
|
||||
border: 1px solid;
|
||||
`;
|
||||
|
||||
const Bubble = styled.div`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: lightblue;
|
||||
border: 1px solid;
|
||||
`;
|
||||
|
||||
const Drag = styled.div`
|
||||
position: absolute;
|
||||
min-width: 375px;
|
||||
`;
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{ height: "100%" }}>
|
||||
<DragDiv />
|
||||
</div>,
|
||||
document.getElementById("root")
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"outDir": "dist",
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"composite": true,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": false /* to set at a later stage */,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Debugging Options */
|
||||
"traceResolution": false,
|
||||
"listEmittedFiles": false,
|
||||
"listFiles": false,
|
||||
"pretty": true,
|
||||
|
||||
// Due to broken types in indirect dependencies
|
||||
"skipLibCheck": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./src/types",
|
||||
"../../node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": ["src"],
|
||||
"types": ["mocha"],
|
||||
"compileOnSave": false
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,88 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const { ESBuildMinifyPlugin } = require('esbuild-loader');
|
||||
|
||||
module.exports = env => {
|
||||
const environment = env.ENV || 'development';
|
||||
const communityKey = env.COMMUNITY_KEY || '';
|
||||
|
||||
return {
|
||||
entry: './src/index.tsx',
|
||||
output: {
|
||||
filename: 'index.[fullhash].js',
|
||||
path: path.join(__dirname, 'dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
fallback: {
|
||||
buffer: require.resolve('buffer/'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
assert: require.resolve('assert/'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
zlib: require.resolve('browserify-zlib')
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'esbuild-loader',
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
loader: 'tsx',
|
||||
target: 'es2020',
|
||||
},
|
||||
},
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'source-map-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif|woff|woff2|eot|ttf|otf|ico)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new ESBuildMinifyPlugin({
|
||||
target: 'es2020',
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html',
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.ENV': JSON.stringify(environment),
|
||||
'process.env.COMMUNITY_KEY': JSON.stringify(communityKey),
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser.js',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
host: '0.0.0.0',
|
||||
stats: 'errors-only',
|
||||
overlay: true,
|
||||
hot: true,
|
||||
},
|
||||
stats: 'minimal',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": "./tsconfig.json" },
|
||||
"env": { "es6": true },
|
||||
"ignorePatterns": ["node_modules", "dist", "coverage", "proto"],
|
||||
"plugins": ["import", "eslint-comments", "functional"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier"
|
||||
],
|
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"eslint-comments/disable-enable-pair": [
|
||||
"error",
|
||||
{ "allowWholeFile": true }
|
||||
],
|
||||
"eslint-comments/no-unused-disable": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
|
||||
],
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true }
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.spec.ts", "**/test_utils/*.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/*.spec.ts",
|
||||
"require": "ts-node/register",
|
||||
"exit": true
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# package.json is formatted by package managers, so we ignore it here
|
||||
package.json
|
|
@ -0,0 +1 @@
|
|||
#React chat example
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"name": "@waku/react-group-chat-sdk-example",
|
||||
"main": "index.js",
|
||||
"version": "0.1.0",
|
||||
"repository": "https://github.com/status-im/wakuconnect-chat-sdk/",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"packageManager": "yarn@3.0.1",
|
||||
"scripts": {
|
||||
"clean:all": "yarn clean && rimraf node_modules/",
|
||||
"clean": "rimraf dist/",
|
||||
"build": "rm -rf dist && webpack --mode=production --env ENV=production",
|
||||
"start": "webpack serve --mode=development --env ENV=$ENV COMMUNITY_KEY=$COMMUNITY_KEY --https",
|
||||
"fix": "run-s 'fix:*'",
|
||||
"fix:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --write",
|
||||
"fix:lint": "eslint './{src,test}/**/*.{ts,tsx}' --fix",
|
||||
"test": "run-s 'test:*'",
|
||||
"test:lint": "eslint './{src,test}/**/*.{ts,tsx}'",
|
||||
"test:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --list-different"
|
||||
},
|
||||
"dependencies": {
|
||||
"@status-im/react": "^0.0.0",
|
||||
"assert": "^2.0.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"process": "^0.11.10",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"styled-components": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-hooks": "^7.0.1",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.4.12",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router": "^5.1.16",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/styled-components": "^5.1.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"chai": "^4.3.4",
|
||||
"css-loader": "^6.3.0",
|
||||
"esbuild-loader": "^2.15.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-hooks": "^0.2.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.1",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jsdom": "^16.7.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"mocha": "^9.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"style-loader": "^3.3.0",
|
||||
"ts-loader": "^9.2.5",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.48.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/* /index.html 200
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>Waku Connect group chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script type="module" src="/index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,135 @@
|
|||
import { darkTheme, GroupChat, lightTheme } from "@waku/react-chat-sdk";
|
||||
import React, { useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import styled from "styled-components";
|
||||
|
||||
const fetchMetadata = async (link: string) => {
|
||||
const response = await fetch("https://localhost:3000", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ site: link }),
|
||||
});
|
||||
const body = await response.text();
|
||||
const parsedBody = JSON.parse(body);
|
||||
if (
|
||||
"og:image" in parsedBody &&
|
||||
"og:site_name" in parsedBody &&
|
||||
"og:title" in parsedBody
|
||||
) {
|
||||
return JSON.parse(body);
|
||||
}
|
||||
};
|
||||
|
||||
function DragDiv() {
|
||||
const [x, setX] = useState(0);
|
||||
const [y, setY] = useState(0);
|
||||
const [width, setWidth] = useState(window.innerWidth - 50);
|
||||
const [height, setHeight] = useState(window.innerHeight - 50);
|
||||
const [showChat, setShowChat] = useState(true);
|
||||
const ref = useRef<HTMLHeadingElement>(null);
|
||||
const moved = useRef(false);
|
||||
const setting = useRef("");
|
||||
|
||||
const [theme, setTheme] = useState(true);
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (setting.current === "position") {
|
||||
e.preventDefault();
|
||||
setX(e.x - 20);
|
||||
setY(e.y - 20);
|
||||
}
|
||||
if (setting.current === "size") {
|
||||
setWidth(e.x - x);
|
||||
setHeight(e.y - y);
|
||||
e.preventDefault();
|
||||
}
|
||||
moved.current = true;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
if (!moved.current) [setShowChat((prev) => !prev)];
|
||||
moved.current = false;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setTheme(!theme);
|
||||
}}
|
||||
>
|
||||
Change theme
|
||||
</button>
|
||||
<Drag style={{ left: x, top: y, width: width, height: height }} ref={ref}>
|
||||
<Bubble
|
||||
onMouseDown={() => {
|
||||
setting.current = "position";
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
/>
|
||||
<FloatingDiv className={showChat ? "" : "hide"}>
|
||||
<GroupChat
|
||||
theme={theme ? lightTheme : darkTheme}
|
||||
config={{
|
||||
environment: process.env.ENV ?? "",
|
||||
dappUrl: "https://0.0.0.0:8080",
|
||||
}}
|
||||
fetchMetadata={fetchMetadata}
|
||||
/>
|
||||
</FloatingDiv>
|
||||
{showChat && (
|
||||
<SizeSet
|
||||
onMouseDown={() => {
|
||||
setting.current = "size";
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
></SizeSet>
|
||||
)}
|
||||
</Drag>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FloatingDiv = styled.div`
|
||||
height: calc(100% - 50px);
|
||||
border: 1px solid black;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SizeSet = styled.div`
|
||||
margin-left: auto;
|
||||
margin-right: 0px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: light-grey;
|
||||
border: 1px solid;
|
||||
`;
|
||||
|
||||
const Bubble = styled.div`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: lightblue;
|
||||
border: 1px solid;
|
||||
`;
|
||||
|
||||
const Drag = styled.div`
|
||||
position: absolute;
|
||||
min-width: 375px;
|
||||
`;
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{ height: "100%" }}>
|
||||
<DragDiv />
|
||||
</div>,
|
||||
document.getElementById("root")
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"outDir": "dist",
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"composite": true,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": false /* to set at a later stage */,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Debugging Options */
|
||||
"traceResolution": false,
|
||||
"listEmittedFiles": false,
|
||||
"listFiles": false,
|
||||
"pretty": true,
|
||||
|
||||
// Due to broken types in indirect dependencies
|
||||
"skipLibCheck": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./src/types",
|
||||
"../../node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": ["src"],
|
||||
"types": ["mocha"],
|
||||
"compileOnSave": false
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,88 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const { ESBuildMinifyPlugin } = require('esbuild-loader');
|
||||
|
||||
module.exports = env => {
|
||||
const environment = env.ENV || 'development';
|
||||
const communityKey = env.COMMUNITY_KEY || '';
|
||||
|
||||
return {
|
||||
entry: './src/index.tsx',
|
||||
output: {
|
||||
filename: 'index.[fullhash].js',
|
||||
path: path.join(__dirname, 'dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
fallback: {
|
||||
buffer: require.resolve('buffer/'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
assert: require.resolve('assert/'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
zlib: require.resolve('browserify-zlib')
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'esbuild-loader',
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
loader: 'tsx',
|
||||
target: 'es2020',
|
||||
},
|
||||
},
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'source-map-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif|woff|woff2|eot|ttf|otf|ico)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new ESBuildMinifyPlugin({
|
||||
target: 'es2020',
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html',
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.ENV': JSON.stringify(environment),
|
||||
'process.env.COMMUNITY_KEY': JSON.stringify(communityKey),
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser.js',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
host: '0.0.0.0',
|
||||
stats: 'errors-only',
|
||||
overlay: true,
|
||||
hot: true,
|
||||
},
|
||||
stats: 'minimal',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": "./tsconfig.json" },
|
||||
"env": { "es6": true },
|
||||
"ignorePatterns": ["node_modules", "dist", "coverage", "proto"],
|
||||
"plugins": ["import", "eslint-comments", "functional"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier"
|
||||
],
|
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": ["error"],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"eslint-comments/disable-enable-pair": [
|
||||
"error",
|
||||
{ "allowWholeFile": true }
|
||||
],
|
||||
"eslint-comments/no-unused-disable": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
|
||||
],
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true }
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.spec.ts", "**/test_utils/*.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/*.spec.ts",
|
||||
"require": "ts-node/register",
|
||||
"exit": true
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# package.json is formatted by package managers, so we ignore it here
|
||||
package.json
|
|
@ -0,0 +1 @@
|
|||
# `status-core`
|
|
@ -0,0 +1,6 @@
|
|||
version: v1beta1
|
||||
|
||||
plugins:
|
||||
- name: ts_proto
|
||||
out: ./src/proto
|
||||
opt: grpc_js,esModuleInterop=true
|
|
@ -0,0 +1,9 @@
|
|||
version: v1beta1
|
||||
|
||||
build:
|
||||
roots:
|
||||
- ./proto
|
||||
lint:
|
||||
except:
|
||||
- ENUM_ZERO_VALUE_SUFFIX
|
||||
- ENUM_VALUE_PREFIX
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "@status-im/core",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"repository": {
|
||||
"url": "https://github.com/status-im/status-web.git",
|
||||
"directory": "packages/status-core",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/status-im/status-web/issues"
|
||||
},
|
||||
"main": "dist/cjs/src/index.js",
|
||||
"module": "dist/esm/src/index.js",
|
||||
"types": "dist/esm/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "run-s 'build:*'",
|
||||
"build:esm": "tsc --module es2020 --target es2017 --outDir dist/esm",
|
||||
"build:cjs": "tsc --outDir dist/cjs",
|
||||
"fix": "run-s 'fix:*'",
|
||||
"fix:prettier": "prettier \"src/**/*.ts\" \"./*.json\" --write",
|
||||
"fix:lint": "eslint src --ext .ts --fix",
|
||||
"test": "run-s 'test:*'",
|
||||
"test:lint": "eslint src --ext .ts",
|
||||
"test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" --list-different",
|
||||
"test:unit": "mocha",
|
||||
"proto": "run-s 'proto:*'",
|
||||
"proto:lint": "buf lint",
|
||||
"proto:build": "buf generate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/elliptic": "^6.4.14",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/pbkdf2": "^3.1.0",
|
||||
"@types/secp256k1": "^4.0.3",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||
"@typescript-eslint/parser": "^4.31.1",
|
||||
"chai": "^4.3.4",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-functional": "^3.7.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"mocha": "^9.1.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.4.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"ts-proto": "^1.83.0",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": "^5.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"ecies-geth": "^1.5.3",
|
||||
"elliptic": "^6.5.4",
|
||||
"js-sha3": "^0.8.0",
|
||||
"js-waku": "^0.16.0",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"protobufjs": "^6.11.2",
|
||||
"secp256k1": "^4.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
import "communities/v1/enums.proto";
|
||||
|
||||
// ChatIdentity represents the user defined identity associated with their public chat key
|
||||
message ChatIdentity {
|
||||
// Lamport timestamp of the message
|
||||
uint64 clock = 1;
|
||||
|
||||
// ens_name is the valid ENS name associated with the chat key
|
||||
string ens_name = 2;
|
||||
|
||||
// images is a string indexed mapping of images associated with an identity
|
||||
map<string, IdentityImage> images = 3;
|
||||
|
||||
// display name is the user set identity, valid only for organisations
|
||||
string display_name = 4;
|
||||
|
||||
// description is the user set description, valid only for organisations
|
||||
string description = 5;
|
||||
|
||||
string color = 6;
|
||||
|
||||
string emoji = 7;
|
||||
}
|
||||
|
||||
// ProfileImage represents data associated with a user's profile image
|
||||
message IdentityImage {
|
||||
|
||||
// payload is a context based payload for the profile image data,
|
||||
// context is determined by the `source_type`
|
||||
bytes payload = 1;
|
||||
|
||||
// source_type signals the image payload source
|
||||
SourceType source_type = 2;
|
||||
|
||||
// image_type signals the image type and method of parsing the payload
|
||||
ImageType image_type =3;
|
||||
|
||||
// SourceType are the predefined types of image source allowed
|
||||
enum SourceType {
|
||||
UNKNOWN_SOURCE_TYPE = 0;
|
||||
|
||||
// RAW_PAYLOAD image byte data
|
||||
RAW_PAYLOAD = 1;
|
||||
|
||||
// ENS_AVATAR uses the ENS record's resolver get-text-data.avatar data
|
||||
// The `payload` field will be ignored if ENS_AVATAR is selected
|
||||
// The application will read and parse the ENS avatar data as image payload data, URLs will be ignored
|
||||
// The parent `ChatMessageIdentity` must have a valid `ens_name` set
|
||||
ENS_AVATAR = 2;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
import "communities/v1/enums.proto";
|
||||
|
||||
message StickerMessage {
|
||||
string hash = 1;
|
||||
int32 pack = 2;
|
||||
}
|
||||
|
||||
message ImageMessage {
|
||||
bytes payload = 1;
|
||||
ImageType type = 2;
|
||||
}
|
||||
|
||||
message AudioMessage {
|
||||
bytes payload = 1;
|
||||
AudioType type = 2;
|
||||
uint64 duration_ms = 3;
|
||||
enum AudioType {
|
||||
AUDIO_TYPE_UNKNOWN_UNSPECIFIED = 0;
|
||||
AUDIO_TYPE_AAC = 1;
|
||||
AUDIO_TYPE_AMR = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message EditMessage {
|
||||
uint64 clock = 1;
|
||||
// Text of the message
|
||||
string text = 2;
|
||||
|
||||
string chat_id = 3;
|
||||
string message_id = 4;
|
||||
|
||||
// Grant for community edit messages
|
||||
bytes grant = 5;
|
||||
|
||||
// The type of message (public/one-to-one/private-group-chat)
|
||||
MessageType message_type = 6;
|
||||
}
|
||||
|
||||
message DeleteMessage {
|
||||
uint64 clock = 1;
|
||||
|
||||
string chat_id = 2;
|
||||
string message_id = 3;
|
||||
|
||||
// Grant for community delete messages
|
||||
bytes grant = 4;
|
||||
|
||||
// The type of message (public/one-to-one/private-group-chat)
|
||||
MessageType message_type = 5;
|
||||
}
|
||||
|
||||
|
||||
message ChatMessage {
|
||||
// Lamport timestamp of the chat message
|
||||
uint64 clock = 1;
|
||||
// Unix timestamps in milliseconds, currently not used as we use whisper as more reliable, but here
|
||||
// so that we don't rely on it
|
||||
uint64 timestamp = 2;
|
||||
// Text of the message
|
||||
string text = 3;
|
||||
// Id of the message that we are replying to
|
||||
string response_to = 4;
|
||||
// Ens name of the sender
|
||||
string ens_name = 5;
|
||||
// Chat id, this field is symmetric for public-chats and private group chats,
|
||||
// but asymmetric in case of one-to-ones, as the sender will use the chat-id
|
||||
// of the received, while the receiver will use the chat-id of the sender.
|
||||
// Probably should be the concatenation of sender-pk & receiver-pk in alphabetical order
|
||||
string chat_id = 6;
|
||||
|
||||
// The type of message (public/one-to-one/private-group-chat)
|
||||
MessageType message_type = 7;
|
||||
// The type of the content of the message
|
||||
ContentType content_type = 8;
|
||||
|
||||
oneof payload {
|
||||
StickerMessage sticker = 9;
|
||||
ImageMessage image = 10;
|
||||
AudioMessage audio = 11;
|
||||
bytes community = 12;
|
||||
}
|
||||
|
||||
// Grant for community chat messages
|
||||
optional bytes grant = 13;
|
||||
|
||||
enum ContentType {
|
||||
CONTENT_TYPE_UNKNOWN_UNSPECIFIED = 0;
|
||||
CONTENT_TYPE_TEXT_PLAIN = 1;
|
||||
CONTENT_TYPE_STICKER = 2;
|
||||
CONTENT_TYPE_STATUS = 3;
|
||||
CONTENT_TYPE_EMOJI = 4;
|
||||
CONTENT_TYPE_TRANSACTION_COMMAND = 5;
|
||||
// Only local
|
||||
CONTENT_TYPE_SYSTEM_MESSAGE_CONTENT_PRIVATE_GROUP = 6;
|
||||
CONTENT_TYPE_IMAGE = 7;
|
||||
CONTENT_TYPE_AUDIO = 8;
|
||||
CONTENT_TYPE_COMMUNITY = 9;
|
||||
// Only local
|
||||
CONTENT_TYPE_SYSTEM_MESSAGE_GAP = 10;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
import "communities/v1/chat_identity.proto";
|
||||
|
||||
message Grant {
|
||||
bytes community_id = 1;
|
||||
bytes member_id = 2;
|
||||
string chat_id = 3;
|
||||
uint64 clock = 4;
|
||||
}
|
||||
|
||||
message CommunityMember {
|
||||
enum Roles {
|
||||
UNKNOWN_ROLE = 0;
|
||||
ROLE_ALL = 1;
|
||||
ROLE_MANAGE_USERS = 2;
|
||||
}
|
||||
repeated Roles roles = 1;
|
||||
}
|
||||
|
||||
message CommunityPermissions {
|
||||
enum Access {
|
||||
UNKNOWN_ACCESS = 0;
|
||||
NO_MEMBERSHIP = 1;
|
||||
INVITATION_ONLY = 2;
|
||||
ON_REQUEST = 3;
|
||||
}
|
||||
|
||||
bool ens_only = 1;
|
||||
// https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md is a candidate for the algorithm to be used in case we want to have private communityal chats, lighter than pairwise encryption using the DR, less secure, but more efficient for large number of participants
|
||||
bool private = 2;
|
||||
Access access = 3;
|
||||
}
|
||||
|
||||
message CommunityDescription {
|
||||
uint64 clock = 1;
|
||||
map<string,CommunityMember> members = 2;
|
||||
CommunityPermissions permissions = 3;
|
||||
ChatIdentity identity = 5;
|
||||
map<string,CommunityChat> chats = 6;
|
||||
repeated string ban_list = 7;
|
||||
map<string,CommunityCategory> categories = 8;
|
||||
}
|
||||
|
||||
message CommunityChat {
|
||||
map<string,CommunityMember> members = 1;
|
||||
CommunityPermissions permissions = 2;
|
||||
ChatIdentity identity = 3;
|
||||
string category_id = 4;
|
||||
int32 position = 5;
|
||||
}
|
||||
|
||||
message CommunityCategory {
|
||||
string category_id = 1;
|
||||
string name = 2;
|
||||
int32 position = 3;
|
||||
}
|
||||
|
||||
message CommunityInvitation {
|
||||
bytes community_description = 1;
|
||||
bytes grant = 2;
|
||||
string chat_id = 3;
|
||||
bytes public_key = 4;
|
||||
}
|
||||
|
||||
message CommunityRequestToJoin {
|
||||
uint64 clock = 1;
|
||||
string ens_name = 2;
|
||||
string chat_id = 3;
|
||||
bytes community_id = 4;
|
||||
}
|
||||
|
||||
message CommunityRequestToJoinResponse {
|
||||
uint64 clock = 1;
|
||||
CommunityDescription community = 2;
|
||||
bool accepted = 3;
|
||||
bytes grant = 4;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
import "communities/v1/enums.proto";
|
||||
|
||||
message EmojiReaction {
|
||||
// clock Lamport timestamp of the chat message
|
||||
uint64 clock = 1;
|
||||
|
||||
// chat_id the ID of the chat the message belongs to, for query efficiency the chat_id is stored in the db even though the
|
||||
// target message also stores the chat_id
|
||||
string chat_id = 2;
|
||||
|
||||
// message_id the ID of the target message that the user wishes to react to
|
||||
string message_id = 3;
|
||||
|
||||
// message_type is (somewhat confusingly) the ID of the type of chat the message belongs to
|
||||
MessageType message_type = 4;
|
||||
|
||||
// type the ID of the emoji the user wishes to react with
|
||||
Type type = 5;
|
||||
|
||||
enum Type {
|
||||
UNKNOWN_EMOJI_REACTION_TYPE = 0;
|
||||
LOVE = 1;
|
||||
THUMBS_UP = 2;
|
||||
THUMBS_DOWN = 3;
|
||||
LAUGH = 4;
|
||||
SAD = 5;
|
||||
ANGRY = 6;
|
||||
}
|
||||
|
||||
// whether this is a rectraction of a previously sent emoji
|
||||
bool retracted = 6;
|
||||
|
||||
// Grant for organisation chat messages
|
||||
bytes grant = 7;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
enum MessageType {
|
||||
MESSAGE_TYPE_UNKNOWN_UNSPECIFIED = 0;
|
||||
MESSAGE_TYPE_ONE_TO_ONE = 1;
|
||||
MESSAGE_TYPE_MESSAGE_TYPE_PUBLIC_GROUP = 2;
|
||||
MESSAGE_TYPE_PRIVATE_GROUP = 3;
|
||||
// Only local
|
||||
MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP = 4;
|
||||
MESSAGE_TYPE_COMMUNITY_CHAT = 5;
|
||||
// Only local
|
||||
MESSAGE_TYPE_SYSTEM_MESSAGE_GAP = 6;
|
||||
}
|
||||
|
||||
enum ImageType {
|
||||
IMAGE_TYPE_UNKNOWN_UNSPECIFIED = 0;
|
||||
|
||||
// Raster image files is payload data that can be read as a raster image
|
||||
IMAGE_TYPE_PNG = 1;
|
||||
IMAGE_TYPE_JPEG = 2;
|
||||
IMAGE_TYPE_WEBP = 3;
|
||||
IMAGE_TYPE_GIF = 4;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
import "communities/v1/chat_message.proto";
|
||||
import "communities/v1/emoji_reaction.proto";
|
||||
|
||||
message MembershipUpdateEvent {
|
||||
// Lamport timestamp of the event
|
||||
uint64 clock = 1;
|
||||
// List of public keys of objects of the action
|
||||
repeated string members = 2;
|
||||
// Name of the chat for the CHAT_CREATED/NAME_CHANGED event types
|
||||
string name = 3;
|
||||
// The type of the event
|
||||
EventType type = 4;
|
||||
|
||||
enum EventType {
|
||||
UNKNOWN = 0;
|
||||
CHAT_CREATED = 1;
|
||||
NAME_CHANGED = 2;
|
||||
MEMBERS_ADDED = 3;
|
||||
MEMBER_JOINED = 4;
|
||||
MEMBER_REMOVED = 5;
|
||||
ADMINS_ADDED = 6;
|
||||
ADMIN_REMOVED = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// MembershipUpdateMessage is a message used to propagate information
|
||||
// about group membership changes.
|
||||
// For more information, see https://github.com/status-im/specs/blob/master/status-group-chats-spec.md.
|
||||
message MembershipUpdateMessage {
|
||||
// The chat id of the private group chat
|
||||
string chat_id = 1;
|
||||
// A list of events for this group chat, first x bytes are the signature, then is a
|
||||
// protobuf encoded MembershipUpdateEvent
|
||||
repeated bytes events = 2;
|
||||
|
||||
// An optional chat message
|
||||
oneof chat_entity {
|
||||
ChatMessage message = 3;
|
||||
EmojiReaction emoji_reaction = 4;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package communities.v1;
|
||||
|
||||
/* Specs:
|
||||
:AUTOMATIC
|
||||
To Send - "AUTOMATIC" status ping every 5 minutes
|
||||
Display - Online for up to 5 minutes from the last clock, after that Offline
|
||||
:ALWAYS_ONLINE
|
||||
To Send - "ALWAYS_ONLINE" status ping every 5 minutes
|
||||
Display - Online for up to 2 weeks from the last clock, after that Offline
|
||||
:INACTIVE
|
||||
To Send - A single "INACTIVE" status ping
|
||||
Display - Offline forever
|
||||
Note: Only send pings if the user interacted with the app in the last x minutes. */
|
||||
message StatusUpdate {
|
||||
|
||||
uint64 clock = 1;
|
||||
|
||||
StatusType status_type = 2;
|
||||
|
||||
string custom_text = 3;
|
||||
|
||||
enum StatusType {
|
||||
UNKNOWN_STATUS_TYPE = 0;
|
||||
AUTOMATIC = 1;
|
||||
DO_NOT_DISTURB = 2;
|
||||
ALWAYS_ONLINE = 3;
|
||||
INACTIVE = 4;
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package status.v1;
|
||||
|
||||
message ApplicationMetadataMessage {
|
||||
// Signature of the payload field
|
||||
bytes signature = 1;
|
||||
// This is the encoded protobuf of the application level message, i.e ChatMessage
|
||||
bytes payload = 2;
|
||||
|
||||
// The type of protobuf message sent
|
||||
Type type = 3;
|
||||
|
||||
enum Type {
|
||||
TYPE_UNKNOWN_UNSPECIFIED = 0;
|
||||
TYPE_CHAT_MESSAGE = 1;
|
||||
TYPE_CONTACT_UPDATE = 2;
|
||||
TYPE_MEMBERSHIP_UPDATE_MESSAGE = 3;
|
||||
TYPE_PAIR_INSTALLATION = 4;
|
||||
TYPE_SYNC_INSTALLATION = 5;
|
||||
TYPE_REQUEST_ADDRESS_FOR_TRANSACTION = 6;
|
||||
TYPE_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION = 7;
|
||||
TYPE_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION = 8;
|
||||
TYPE_REQUEST_TRANSACTION = 9;
|
||||
TYPE_SEND_TRANSACTION = 10;
|
||||
TYPE_DECLINE_REQUEST_TRANSACTION = 11;
|
||||
TYPE_SYNC_INSTALLATION_CONTACT = 12;
|
||||
TYPE_SYNC_INSTALLATION_ACCOUNT = 13;
|
||||
TYPE_SYNC_INSTALLATION_PUBLIC_CHAT = 14;
|
||||
TYPE_CONTACT_CODE_ADVERTISEMENT = 15;
|
||||
TYPE_PUSH_NOTIFICATION_REGISTRATION = 16;
|
||||
TYPE_PUSH_NOTIFICATION_REGISTRATION_RESPONSE = 17;
|
||||
TYPE_PUSH_NOTIFICATION_QUERY = 18;
|
||||
TYPE_PUSH_NOTIFICATION_QUERY_RESPONSE = 19;
|
||||
TYPE_PUSH_NOTIFICATION_REQUEST = 20;
|
||||
TYPE_PUSH_NOTIFICATION_RESPONSE = 21;
|
||||
TYPE_EMOJI_REACTION = 22;
|
||||
TYPE_GROUP_CHAT_INVITATION = 23;
|
||||
TYPE_CHAT_IDENTITY = 24;
|
||||
TYPE_COMMUNITY_DESCRIPTION = 25;
|
||||
TYPE_COMMUNITY_INVITATION = 26;
|
||||
TYPE_COMMUNITY_REQUEST_TO_JOIN = 27;
|
||||
TYPE_PIN_MESSAGE = 28;
|
||||
TYPE_EDIT_MESSAGE = 29;
|
||||
TYPE_STATUS_UPDATE = 30;
|
||||
TYPE_DELETE_MESSAGE = 31;
|
||||
TYPE_SYNC_INSTALLATION_COMMUNITY = 32;
|
||||
TYPE_ANONYMOUS_METRIC_BATCH = 33;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { idToContentTopic } from "./contentTopic";
|
||||
import { createSymKeyFromPassword } from "./encryption";
|
||||
import { ChatMessage, Content } from "./wire/chat_message";
|
||||
import { CommunityChat } from "./wire/community_chat";
|
||||
|
||||
/**
|
||||
* Represent a chat room. Only public chats are currently supported.
|
||||
*/
|
||||
export class Chat {
|
||||
private lastClockValue?: number;
|
||||
private lastMessage?: ChatMessage;
|
||||
|
||||
private constructor(
|
||||
public id: string,
|
||||
public symKey: Uint8Array,
|
||||
public communityChat?: CommunityChat
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a public chat room.
|
||||
* [[Community.instantiateChat]] MUST be used for chats belonging to a community.
|
||||
*/
|
||||
public static async create(
|
||||
id: string,
|
||||
communityChat?: CommunityChat
|
||||
): Promise<Chat> {
|
||||
const symKey = await createSymKeyFromPassword(id);
|
||||
|
||||
return new Chat(id, symKey, communityChat);
|
||||
}
|
||||
|
||||
public get contentTopic(): string {
|
||||
return idToContentTopic(this.id);
|
||||
}
|
||||
|
||||
public createMessage(content: Content, responseTo?: string): ChatMessage {
|
||||
const { timestamp, clock } = this._nextClockAndTimestamp();
|
||||
|
||||
const message = ChatMessage.createMessage(
|
||||
clock,
|
||||
timestamp,
|
||||
this.id,
|
||||
content,
|
||||
responseTo
|
||||
);
|
||||
|
||||
this._updateClockFromMessage(message);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public handleNewMessage(message: ChatMessage): void {
|
||||
this._updateClockFromMessage(message);
|
||||
}
|
||||
|
||||
private _nextClockAndTimestamp(): { clock: number; timestamp: number } {
|
||||
let clock = this.lastClockValue;
|
||||
const timestamp = Date.now();
|
||||
|
||||
if (!clock || clock < timestamp) {
|
||||
clock = timestamp;
|
||||
} else {
|
||||
clock += 1;
|
||||
}
|
||||
|
||||
return { clock, timestamp };
|
||||
}
|
||||
|
||||
private _updateClockFromMessage(message: ChatMessage): void {
|
||||
if (
|
||||
!this.lastMessage ||
|
||||
!this.lastMessage.clock ||
|
||||
(message.clock && this.lastMessage.clock <= message.clock)
|
||||
) {
|
||||
this.lastMessage = message;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.lastClockValue ||
|
||||
(message.clock && this.lastClockValue < message.clock)
|
||||
) {
|
||||
this.lastClockValue = message.clock;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { expect } from "chai";
|
||||
import { Waku } from "js-waku";
|
||||
|
||||
import { Community } from "./community";
|
||||
import { CommunityDescription } from "./wire/community_description";
|
||||
|
||||
describe("Community [live data]", () => {
|
||||
before(function () {
|
||||
if (process.env.CI) {
|
||||
// Skip live data test in CI
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("Retrieves community description For DappConnect Test from Waku prod fleet", async function () {
|
||||
this.timeout(20000);
|
||||
const waku = await Waku.create({ bootstrap: { default: true } });
|
||||
|
||||
await waku.waitForRemotePeer();
|
||||
|
||||
const community = await Community.instantiateCommunity(
|
||||
"0x02cf13719c8b836bebd4e430c497ee38e798a43e4d8c4760c34bbd9bf4f2434d26",
|
||||
waku
|
||||
);
|
||||
const desc = community.description as CommunityDescription;
|
||||
expect(desc).to.not.be.undefined;
|
||||
|
||||
expect(desc.identity?.displayName).to.eq("Test Community");
|
||||
|
||||
const descChats = Array.from(desc.chats.values()).map(
|
||||
(chat) => chat?.identity?.displayName
|
||||
);
|
||||
expect(descChats).to.include("Test Chat");
|
||||
expect(descChats).to.include("Additional Chat");
|
||||
|
||||
const chats = Array.from(community.chats.values()).map(
|
||||
(chat) => chat?.communityChat?.identity?.displayName
|
||||
);
|
||||
expect(chats).to.include("Test Chat");
|
||||
expect(chats).to.include("Additional Chat");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
import debug from "debug";
|
||||
import { Waku } from "js-waku";
|
||||
|
||||
import { Chat } from "./chat";
|
||||
import { bufToHex, hexToBuf } from "./utils";
|
||||
import { CommunityChat } from "./wire/community_chat";
|
||||
import { CommunityDescription } from "./wire/community_description";
|
||||
|
||||
const dbg = debug("communities:community");
|
||||
|
||||
export class Community {
|
||||
public publicKey: Uint8Array;
|
||||
private waku: Waku;
|
||||
public chats: Map<string, Chat>; // Chat id, Chat
|
||||
public description?: CommunityDescription;
|
||||
|
||||
constructor(publicKey: Uint8Array, waku: Waku) {
|
||||
this.publicKey = publicKey;
|
||||
this.waku = waku;
|
||||
this.chats = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a Community by retrieving its details from the Waku network.
|
||||
*
|
||||
* This class is used to interact with existing communities only,
|
||||
* the Status Desktop or Mobile app must be used to manage a community.
|
||||
*
|
||||
* @param publicKey The community's public key in hex format.
|
||||
* Can be found in the community's invite link: https://join.status.im/c/<public key>
|
||||
* @param waku The Waku instance, used to retrieve Community information from the network.
|
||||
*/
|
||||
public static async instantiateCommunity(
|
||||
publicKey: string,
|
||||
waku: Waku
|
||||
): Promise<Community> {
|
||||
const community = new Community(hexToBuf(publicKey), waku);
|
||||
|
||||
await community.refreshCommunityDescription();
|
||||
|
||||
return community;
|
||||
}
|
||||
|
||||
public get publicKeyStr(): string {
|
||||
return bufToHex(this.publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and update community information from the network.
|
||||
* Uses most recent community description message available.
|
||||
*/
|
||||
async refreshCommunityDescription(): Promise<void> {
|
||||
const desc = await CommunityDescription.retrieve(
|
||||
this.publicKey,
|
||||
this.waku.store
|
||||
);
|
||||
|
||||
if (!desc) {
|
||||
dbg(`Failed to retrieve Community Description for ${this.publicKeyStr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.description = desc;
|
||||
|
||||
await Promise.all(
|
||||
Array.from(this.description.chats).map(([chatUuid, communityChat]) => {
|
||||
return this.instantiateChat(chatUuid, communityChat);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate [[Chat]] object based on the passed chat name.
|
||||
* The Chat MUST already be part of the Community and the name MUST be exact (including casing).
|
||||
*
|
||||
* @throws string If the Community Description is unavailable or the chat is not found;
|
||||
*/
|
||||
private async instantiateChat(
|
||||
chatUuid: string,
|
||||
communityChat: CommunityChat
|
||||
): Promise<void> {
|
||||
if (!this.description)
|
||||
throw "Failed to retrieve community description, cannot instantiate chat";
|
||||
|
||||
const chatId = this.publicKeyStr + chatUuid;
|
||||
if (this.chats.get(chatId)) return;
|
||||
|
||||
const chat = await Chat.create(chatId, communityChat);
|
||||
|
||||
this.chats.set(chatId, chat);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
import { PageDirection, Waku, WakuMessage } from "js-waku";
|
||||
|
||||
import { idToContactCodeTopic } from "./contentTopic";
|
||||
import { Identity } from "./identity";
|
||||
import { StatusUpdate_StatusType } from "./proto/communities/v1/status_update";
|
||||
import { bufToHex, getLatestUserNickname } from "./utils";
|
||||
import { ChatIdentity } from "./wire/chat_identity";
|
||||
import { StatusUpdate } from "./wire/status_update";
|
||||
|
||||
const STATUS_BROADCAST_INTERVAL = 30000;
|
||||
const NICKNAME_BROADCAST_INTERVAL = 300000;
|
||||
|
||||
export class Contacts {
|
||||
waku: Waku;
|
||||
identity: Identity | undefined;
|
||||
nickname?: string;
|
||||
private callback: (publicKey: string, clock: number) => void;
|
||||
private callbackNickname: (publicKey: string, nickname: string) => void;
|
||||
private contacts: string[] = [];
|
||||
|
||||
/**
|
||||
* Contacts holds a list of user contacts and listens to their status broadcast
|
||||
*
|
||||
* When watched user broadcast callback is called.
|
||||
*
|
||||
* Class also broadcasts own status on contact-code topic
|
||||
*
|
||||
* @param identity identity of user that is used to broadcast status message
|
||||
*
|
||||
* @param waku waku class used to listen to broadcast and broadcast status
|
||||
*
|
||||
* @param callback callback function called when user status broadcast is received
|
||||
*/
|
||||
public constructor(
|
||||
identity: Identity | undefined,
|
||||
waku: Waku,
|
||||
callback: (publicKey: string, clock: number) => void,
|
||||
callbackNickname: (publicKey: string, nickname: string) => void,
|
||||
nickname?: string
|
||||
) {
|
||||
this.waku = waku;
|
||||
this.identity = identity;
|
||||
this.nickname = nickname;
|
||||
this.callback = callback;
|
||||
this.callbackNickname = callbackNickname;
|
||||
this.startBroadcast();
|
||||
if (identity) {
|
||||
this.addContact(bufToHex(identity.publicKey));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add contact to watch list of status broadcast
|
||||
*
|
||||
* When user broadcasts its status callback is called
|
||||
*
|
||||
* @param publicKey public key of user
|
||||
*/
|
||||
public addContact(publicKey: string): void {
|
||||
if (!this.contacts.find((e) => publicKey === e)) {
|
||||
const now = new Date();
|
||||
const callback = (wakuMessage: WakuMessage): void => {
|
||||
if (wakuMessage.payload) {
|
||||
const msg = StatusUpdate.decode(wakuMessage.payload);
|
||||
this.callback(publicKey, msg.clock ?? 0);
|
||||
}
|
||||
};
|
||||
this.contacts.push(publicKey);
|
||||
this.callback(publicKey, 0);
|
||||
this.waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
|
||||
callback: (msgs) => msgs.forEach((e) => callback(e)),
|
||||
timeFilter: {
|
||||
startTime: new Date(now.getTime() - STATUS_BROADCAST_INTERVAL * 2),
|
||||
endTime: now,
|
||||
},
|
||||
});
|
||||
this.waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
|
||||
callback: (msgs) =>
|
||||
msgs.some((e) => {
|
||||
try {
|
||||
if (e.payload) {
|
||||
const chatIdentity = ChatIdentity.decode(e?.payload);
|
||||
if (chatIdentity) {
|
||||
this.callbackNickname(
|
||||
publicKey,
|
||||
chatIdentity?.displayName ?? ""
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
pageDirection: PageDirection.BACKWARD,
|
||||
});
|
||||
this.waku.relay.addObserver(callback, [idToContactCodeTopic(publicKey)]);
|
||||
}
|
||||
}
|
||||
|
||||
private startBroadcast(): void {
|
||||
const send = async (): Promise<void> => {
|
||||
if (this.identity) {
|
||||
const statusUpdate = StatusUpdate.create(
|
||||
StatusUpdate_StatusType.AUTOMATIC,
|
||||
""
|
||||
);
|
||||
const msg = await WakuMessage.fromBytes(
|
||||
statusUpdate.encode(),
|
||||
idToContactCodeTopic(bufToHex(this.identity.publicKey))
|
||||
);
|
||||
this.waku.relay.send(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNickname = async (): Promise<void> => {
|
||||
if (this.identity) {
|
||||
const now = new Date().getTime();
|
||||
const { clock, nickname: newNickname } = await getLatestUserNickname(
|
||||
this.identity.publicKey,
|
||||
this.waku
|
||||
);
|
||||
|
||||
if (this.nickname) {
|
||||
if (this.nickname !== newNickname) {
|
||||
await sendNickname();
|
||||
} else {
|
||||
if (clock < now - NICKNAME_BROADCAST_INTERVAL) {
|
||||
await sendNickname();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.nickname = newNickname;
|
||||
this.callbackNickname(bufToHex(this.identity.publicKey), newNickname);
|
||||
if (clock < now - NICKNAME_BROADCAST_INTERVAL) {
|
||||
await sendNickname();
|
||||
}
|
||||
}
|
||||
}
|
||||
setInterval(send, NICKNAME_BROADCAST_INTERVAL);
|
||||
};
|
||||
|
||||
const sendNickname = async (): Promise<void> => {
|
||||
if (this.identity) {
|
||||
const publicKey = bufToHex(this.identity.publicKey);
|
||||
if (this.nickname) {
|
||||
const chatIdentity = new ChatIdentity({
|
||||
clock: new Date().getTime(),
|
||||
color: "",
|
||||
description: "",
|
||||
emoji: "",
|
||||
images: {},
|
||||
ensName: "",
|
||||
displayName: this?.nickname ?? "",
|
||||
});
|
||||
const msg = await WakuMessage.fromBytes(
|
||||
chatIdentity.encode(),
|
||||
idToContactCodeTopic(publicKey),
|
||||
{ sigPrivKey: this.identity.privateKey }
|
||||
);
|
||||
await this.waku.relay.send(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
handleNickname();
|
||||
send();
|
||||
setInterval(send, STATUS_BROADCAST_INTERVAL);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Buffer } from "buffer";
|
||||
|
||||
import { keccak256 } from "js-sha3";
|
||||
|
||||
const TopicLength = 4;
|
||||
|
||||
/**
|
||||
* Get the content topic of for a given Chat or Community
|
||||
* @param id The Chat id or Community id (hex string prefixed with 0x).
|
||||
* @returns string The Waku v2 Content Topic.
|
||||
*/
|
||||
export function idToContentTopic(id: string): string {
|
||||
const hash = keccak256.arrayBuffer(id);
|
||||
|
||||
const topic = Buffer.from(hash).slice(0, TopicLength);
|
||||
|
||||
return "/waku/1/" + "0x" + topic.toString("hex") + "/rfc26";
|
||||
}
|
||||
|
||||
export function idToContactCodeTopic(id: string): string {
|
||||
return idToContentTopic(id + "-contact-code");
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { expect } from "chai";
|
||||
|
||||
import { createSymKeyFromPassword } from "./encryption";
|
||||
|
||||
describe("Encryption", () => {
|
||||
it("Generate symmetric key from password", async function () {
|
||||
const str = "arbitrary data here";
|
||||
const symKey = await createSymKeyFromPassword(str);
|
||||
|
||||
expect(Buffer.from(symKey).toString("hex")).to.eq(
|
||||
"c49ad65ebf2a7b7253bf400e3d27719362a91b2c9b9f54d50a69117021666c33"
|
||||
);
|
||||
});
|
||||
|
||||
it("Generate symmetric key from password for chat", async function () {
|
||||
const str =
|
||||
"0x02dcec6041fb999d65f1d33363e08c93d3c1f6f0fbbb26add383e2cf46c2b921f41dc14fd8-9a8b-4df5-a358-2c3067be5439";
|
||||
const symKey = await createSymKeyFromPassword(str);
|
||||
|
||||
expect(Buffer.from(symKey).toString("hex")).to.eq(
|
||||
"76ff5bf0a74a8e724367c7fc003f066d477641f468768a8da2817addf5c2ce76"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import pbkdf2 from "pbkdf2";
|
||||
|
||||
const AESKeyLength = 32; // bytes
|
||||
|
||||
export async function createSymKeyFromPassword(
|
||||
password: string
|
||||
): Promise<Uint8Array> {
|
||||
return pbkdf2.pbkdf2Sync(
|
||||
Buffer.from(password, "utf-8"),
|
||||
"",
|
||||
65356,
|
||||
AESKeyLength,
|
||||
"sha256"
|
||||
);
|
||||
}
|
|
@ -0,0 +1,459 @@
|
|||
import { Waku, WakuMessage } from "js-waku";
|
||||
import { DecryptionMethod } from "js-waku/build/main/lib/waku_message";
|
||||
|
||||
import { createSymKeyFromPassword } from "./encryption";
|
||||
import { Identity } from "./identity";
|
||||
import { MembershipUpdateEvent_EventType } from "./proto/communities/v1/membership_update_message";
|
||||
import { getNegotiatedTopic, getPartitionedTopic } from "./topics";
|
||||
import { bufToHex, compressPublicKey } from "./utils";
|
||||
import {
|
||||
MembershipSignedEvent,
|
||||
MembershipUpdateMessage,
|
||||
} from "./wire/membership_update_message";
|
||||
|
||||
import { ChatMessage, Content } from ".";
|
||||
|
||||
type GroupMember = {
|
||||
id: string;
|
||||
topic: string;
|
||||
symKey: Uint8Array;
|
||||
partitionedTopic: string;
|
||||
};
|
||||
|
||||
export type GroupChat = {
|
||||
chatId: string;
|
||||
members: GroupMember[];
|
||||
admins?: string[];
|
||||
name?: string;
|
||||
removed: boolean;
|
||||
};
|
||||
|
||||
export type GroupChatsType = {
|
||||
[id: string]: GroupChat;
|
||||
};
|
||||
/* TODO: add chat messages encryption */
|
||||
|
||||
class GroupChatUsers {
|
||||
private users: { [id: string]: GroupMember } = {};
|
||||
private identity: Identity;
|
||||
|
||||
public constructor(_identity: Identity) {
|
||||
this.identity = _identity;
|
||||
}
|
||||
|
||||
public async getUser(id: string): Promise<GroupMember> {
|
||||
if (this.users[id]) {
|
||||
return this.users[id];
|
||||
}
|
||||
const topic = await getNegotiatedTopic(this.identity, id);
|
||||
const symKey = await createSymKeyFromPassword(topic);
|
||||
const partitionedTopic = getPartitionedTopic(id);
|
||||
const groupUser: GroupMember = { topic, symKey, id, partitionedTopic };
|
||||
this.users[id] = groupUser;
|
||||
return groupUser;
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupChats {
|
||||
waku: Waku;
|
||||
identity: Identity;
|
||||
private callback: (chats: GroupChat) => void;
|
||||
private removeCallback: (chats: GroupChat) => void;
|
||||
private addMessage: (message: ChatMessage, sender: string) => void;
|
||||
private groupChatUsers;
|
||||
|
||||
public chats: GroupChatsType = {};
|
||||
/**
|
||||
* GroupChats holds a list of private chats and listens to their status broadcast
|
||||
*
|
||||
* @param identity identity of user
|
||||
*
|
||||
* @param waku waku class used to listen to broadcast and broadcast status
|
||||
*
|
||||
* @param callback callback function called when new private group chat is ceated
|
||||
*
|
||||
* @param removeCallback callback function when private group chat is to be removed
|
||||
*
|
||||
* @param addMessage callback function when
|
||||
*/
|
||||
public constructor(
|
||||
identity: Identity,
|
||||
waku: Waku,
|
||||
callback: (chat: GroupChat) => void,
|
||||
removeCallback: (chat: GroupChat) => void,
|
||||
addMessage: (message: ChatMessage, sender: string) => void
|
||||
) {
|
||||
this.waku = waku;
|
||||
this.identity = identity;
|
||||
this.groupChatUsers = new GroupChatUsers(identity);
|
||||
this.callback = callback;
|
||||
this.removeCallback = removeCallback;
|
||||
this.addMessage = addMessage;
|
||||
this.listen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send chat message on given private chat
|
||||
*
|
||||
* @param chatId chat id of private group chat
|
||||
*
|
||||
* @param text text message to send
|
||||
*/
|
||||
public async sendMessage(
|
||||
chatId: string,
|
||||
content: Content,
|
||||
responseTo?: string
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
const chat = this.chats[chatId];
|
||||
if (chat) {
|
||||
await Promise.all(
|
||||
chat.members.map(async (member) => {
|
||||
const chatMessage = ChatMessage.createMessage(
|
||||
now,
|
||||
now,
|
||||
chatId,
|
||||
content,
|
||||
responseTo
|
||||
);
|
||||
const wakuMessage = await WakuMessage.fromBytes(
|
||||
chatMessage.encode(),
|
||||
member.topic,
|
||||
{ sigPrivKey: this.identity.privateKey, symKey: member.symKey }
|
||||
);
|
||||
this.waku.relay.send(wakuMessage);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleUpdateEvent(
|
||||
chatId: string,
|
||||
event: MembershipSignedEvent,
|
||||
useCallback: boolean
|
||||
): Promise<void> {
|
||||
const signer = event.signer ? bufToHex(event.signer) : "";
|
||||
const thisUser = bufToHex(this.identity.publicKey);
|
||||
const chat: GroupChat | undefined = this.chats[chatId];
|
||||
if (signer) {
|
||||
switch (event.event.type) {
|
||||
case MembershipUpdateEvent_EventType.CHAT_CREATED: {
|
||||
const members: GroupMember[] = [];
|
||||
await Promise.all(
|
||||
event.event.members.map(async (member) => {
|
||||
members.push(await this.groupChatUsers.getUser(member));
|
||||
})
|
||||
);
|
||||
await this.addChat(
|
||||
{
|
||||
chatId: chatId,
|
||||
members,
|
||||
admins: [signer],
|
||||
removed: false,
|
||||
},
|
||||
useCallback
|
||||
);
|
||||
break;
|
||||
}
|
||||
case MembershipUpdateEvent_EventType.MEMBER_REMOVED: {
|
||||
if (chat) {
|
||||
chat.members = chat.members.filter(
|
||||
(member) => !event.event.members.includes(member.id)
|
||||
);
|
||||
if (event.event.members.includes(thisUser)) {
|
||||
await this.removeChat(
|
||||
{
|
||||
...chat,
|
||||
removed: true,
|
||||
},
|
||||
useCallback
|
||||
);
|
||||
} else {
|
||||
if (!chat.removed && useCallback) {
|
||||
this.callback(this.chats[chatId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MembershipUpdateEvent_EventType.MEMBERS_ADDED: {
|
||||
if (chat && chat.admins?.includes(signer)) {
|
||||
const members: GroupMember[] = [];
|
||||
await Promise.all(
|
||||
event.event.members.map(async (member) => {
|
||||
members.push(await this.groupChatUsers.getUser(member));
|
||||
})
|
||||
);
|
||||
chat.members.push(...members);
|
||||
if (
|
||||
chat.members.findIndex((member) => member.id === thisUser) > -1
|
||||
) {
|
||||
chat.removed = false;
|
||||
await this.addChat(chat, useCallback);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MembershipUpdateEvent_EventType.NAME_CHANGED: {
|
||||
if (chat) {
|
||||
if (chat.admins?.includes(signer)) {
|
||||
chat.name = event.event.name;
|
||||
this.callback(chat);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async decodeUpdateMessage(
|
||||
message: WakuMessage,
|
||||
useCallback: boolean
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (message?.payload) {
|
||||
const membershipUpdate = MembershipUpdateMessage.decode(
|
||||
message.payload
|
||||
);
|
||||
await Promise.all(
|
||||
membershipUpdate.events.map(
|
||||
async (event) =>
|
||||
await this.handleUpdateEvent(
|
||||
membershipUpdate.chatId,
|
||||
event,
|
||||
useCallback
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private handleWakuChatMessage(
|
||||
message: WakuMessage,
|
||||
chat: GroupChat,
|
||||
member: string
|
||||
): void {
|
||||
try {
|
||||
if (message.payload) {
|
||||
const chatMessage = ChatMessage.decode(message.payload);
|
||||
if (chatMessage) {
|
||||
if (chatMessage.chatId === chat.chatId) {
|
||||
let sender = member;
|
||||
if (message.signaturePublicKey) {
|
||||
sender = compressPublicKey(message.signaturePublicKey);
|
||||
}
|
||||
this.addMessage(chatMessage, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleChatObserver(
|
||||
chat: GroupChat,
|
||||
removeObserver?: boolean
|
||||
): Promise<void> {
|
||||
const observerFunction = removeObserver ? "deleteObserver" : "addObserver";
|
||||
await Promise.all(
|
||||
chat.members.map(async (member) => {
|
||||
if (!removeObserver) {
|
||||
this.waku.relay.addDecryptionKey(member.symKey, {
|
||||
method: DecryptionMethod.Symmetric,
|
||||
contentTopics: [member.topic],
|
||||
});
|
||||
}
|
||||
this.waku.relay[observerFunction](
|
||||
(message) => this.handleWakuChatMessage(message, chat, member.id),
|
||||
[member.topic]
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async addChat(chat: GroupChat, useCallback: boolean): Promise<void> {
|
||||
if (this.chats[chat.chatId]) {
|
||||
this.chats[chat.chatId] = chat;
|
||||
if (useCallback) {
|
||||
this.callback(chat);
|
||||
}
|
||||
} else {
|
||||
this.chats[chat.chatId] = chat;
|
||||
if (useCallback) {
|
||||
await this.handleChatObserver(chat);
|
||||
this.callback(chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async removeChat(
|
||||
chat: GroupChat,
|
||||
useCallback: boolean
|
||||
): Promise<void> {
|
||||
this.chats[chat.chatId] = chat;
|
||||
if (useCallback) {
|
||||
await this.handleChatObserver(chat, true);
|
||||
this.removeCallback(chat);
|
||||
}
|
||||
}
|
||||
|
||||
private async listen(): Promise<void> {
|
||||
const topic = getPartitionedTopic(bufToHex(this.identity.publicKey));
|
||||
const messages = await this.waku.store.queryHistory([topic]);
|
||||
messages.sort((a, b) =>
|
||||
(a?.timestamp?.getTime() ?? 0) < (b?.timestamp?.getTime() ?? 0) ? -1 : 1
|
||||
);
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
await this.decodeUpdateMessage(messages[i], false);
|
||||
}
|
||||
this.waku.relay.addObserver(
|
||||
(message) => this.decodeUpdateMessage(message, true),
|
||||
[topic]
|
||||
);
|
||||
await Promise.all(
|
||||
Object.values(this.chats).map(async (chat) => {
|
||||
if (!chat?.removed) {
|
||||
await this.handleChatObserver(chat);
|
||||
this.callback(chat);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async sendUpdateMessage(
|
||||
payload: Uint8Array,
|
||||
members: GroupMember[]
|
||||
): Promise<void> {
|
||||
const wakuMessages = await Promise.all(
|
||||
members.map(
|
||||
async (member) =>
|
||||
await WakuMessage.fromBytes(payload, member.partitionedTopic)
|
||||
)
|
||||
);
|
||||
wakuMessages.forEach((msg) => this.waku.relay.send(msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a change chat name chat membership update message
|
||||
*
|
||||
* @param chatId a chat id to which message is to be sent
|
||||
*
|
||||
* @param name a name which chat should be changed to
|
||||
*/
|
||||
public async changeChatName(chatId: string, name: string): Promise<void> {
|
||||
const payload = MembershipUpdateMessage.create(chatId, this.identity);
|
||||
const chat = this.chats[chatId];
|
||||
if (chat && payload) {
|
||||
payload.addNameChangeEvent(name);
|
||||
await this.sendUpdateMessage(payload.encode(), chat.members);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a add members group chat membership update message with given members
|
||||
*
|
||||
* @param chatId a chat id to which message is to be sent
|
||||
*
|
||||
* @param members a list of members to be added
|
||||
*/
|
||||
public async addMembers(chatId: string, members: string[]): Promise<void> {
|
||||
const payload = MembershipUpdateMessage.create(chatId, this.identity);
|
||||
const chat = this.chats[chatId];
|
||||
if (chat && payload) {
|
||||
const newMembers: GroupMember[] = [];
|
||||
|
||||
await Promise.all(
|
||||
members
|
||||
.filter(
|
||||
(member) =>
|
||||
!chat.members.map((chatMember) => chatMember.id).includes(member)
|
||||
)
|
||||
.map(async (member) => {
|
||||
newMembers.push(await this.groupChatUsers.getUser(member));
|
||||
})
|
||||
);
|
||||
|
||||
payload.addMembersAddedEvent(newMembers.map((member) => member.id));
|
||||
await this.sendUpdateMessage(payload.encode(), [
|
||||
...chat.members,
|
||||
...newMembers,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a create group chat membership update message with given members
|
||||
*
|
||||
* @param members a list of public keys of members to be included in private group chat
|
||||
*/
|
||||
public async createGroupChat(members: string[]): Promise<void> {
|
||||
const payload = MembershipUpdateMessage.createChat(
|
||||
this.identity,
|
||||
members
|
||||
).encode();
|
||||
|
||||
const newMembers: GroupMember[] = [];
|
||||
|
||||
await Promise.all(
|
||||
members.map(async (member) => {
|
||||
newMembers.push(await this.groupChatUsers.getUser(member));
|
||||
})
|
||||
);
|
||||
|
||||
await this.sendUpdateMessage(payload, newMembers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a remove member to private group chat
|
||||
*
|
||||
* @param chatId id of private group chat
|
||||
*/
|
||||
public async quitChat(chatId: string): Promise<void> {
|
||||
const payload = MembershipUpdateMessage.create(chatId, this.identity);
|
||||
const chat = this.chats[chatId];
|
||||
payload.addMemberRemovedEvent(bufToHex(this.identity.publicKey));
|
||||
await this.sendUpdateMessage(payload.encode(), chat.members);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve previous messages from a Waku Store node for the given chat Id.
|
||||
*
|
||||
*/
|
||||
public async retrievePreviousMessages(
|
||||
chatId: string,
|
||||
startTime: Date,
|
||||
endTime: Date
|
||||
): Promise<number> {
|
||||
const chat = this.chats[chatId];
|
||||
|
||||
if (!chat)
|
||||
throw `Failed to retrieve messages, chat is not joined: ${chatId}`;
|
||||
|
||||
const _callback = (wakuMessages: WakuMessage[], member: string): void => {
|
||||
wakuMessages.forEach((wakuMessage: WakuMessage) =>
|
||||
this.handleWakuChatMessage(wakuMessage, chat, member)
|
||||
);
|
||||
};
|
||||
|
||||
const amountOfMessages: number[] = [];
|
||||
|
||||
await Promise.all(
|
||||
chat.members.map(async (member) => {
|
||||
const msgLength = (
|
||||
await this.waku.store.queryHistory([member.topic], {
|
||||
timeFilter: { startTime, endTime },
|
||||
callback: (msg) => _callback(msg, member.id),
|
||||
decryptionKeys: [member.symKey],
|
||||
})
|
||||
).length;
|
||||
amountOfMessages.push(msgLength);
|
||||
})
|
||||
);
|
||||
return amountOfMessages.reduce((a, b) => a + b);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { Buffer } from "buffer";
|
||||
|
||||
import { keccak256 } from "js-sha3";
|
||||
import { generatePrivateKey } from "js-waku";
|
||||
import * as secp256k1 from "secp256k1";
|
||||
|
||||
import { hexToBuf } from "./utils";
|
||||
|
||||
export class Identity {
|
||||
private pubKey: Uint8Array;
|
||||
public constructor(public privateKey: Uint8Array) {
|
||||
this.pubKey = secp256k1.publicKeyCreate(this.privateKey, true);
|
||||
}
|
||||
|
||||
public static generate(): Identity {
|
||||
const privateKey = generatePrivateKey();
|
||||
return new Identity(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the payload with SHA3-256 and signs the result using the internal private key.
|
||||
*/
|
||||
public sign(payload: Uint8Array): Uint8Array {
|
||||
const hash = keccak256(payload);
|
||||
const { signature, recid } = secp256k1.ecdsaSign(
|
||||
hexToBuf(hash),
|
||||
this.privateKey
|
||||
);
|
||||
|
||||
return Buffer.concat([signature, Buffer.from([recid])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compressed public key.
|
||||
*/
|
||||
public get publicKey(): Uint8Array {
|
||||
return this.pubKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
export { Identity } from "./identity";
|
||||
export { Messenger } from "./messenger";
|
||||
export { Community } from "./community";
|
||||
export { Contacts } from "./contacts";
|
||||
export { Chat } from "./chat";
|
||||
export * from "./groupChats";
|
||||
export * as utils from "./utils";
|
||||
export { ApplicationMetadataMessage } from "./wire/application_metadata_message";
|
||||
export {
|
||||
ChatMessage,
|
||||
ContentType,
|
||||
Content,
|
||||
StickerContent,
|
||||
ImageContent,
|
||||
AudioContent,
|
||||
TextContent,
|
||||
} from "./wire/chat_message";
|
||||
export { getNodesFromHostedJson } from "js-waku";
|
|
@ -0,0 +1,178 @@
|
|||
import { expect } from "chai";
|
||||
import debug from "debug";
|
||||
import { Protocols } from "js-waku/build/main/lib/waku";
|
||||
|
||||
import { Community } from "./community";
|
||||
import { Identity } from "./identity";
|
||||
import { Messenger } from "./messenger";
|
||||
import { bufToHex } from "./utils";
|
||||
import { ApplicationMetadataMessage } from "./wire/application_metadata_message";
|
||||
import { ContentType } from "./wire/chat_message";
|
||||
|
||||
const testChatId = "test-chat-id";
|
||||
|
||||
const dbg = debug("communities:test:messenger");
|
||||
|
||||
describe("Messenger", () => {
|
||||
let messengerAlice: Messenger;
|
||||
let messengerBob: Messenger;
|
||||
let identityAlice: Identity;
|
||||
let identityBob: Identity;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.timeout(20_000);
|
||||
|
||||
dbg("Generate keys");
|
||||
identityAlice = Identity.generate();
|
||||
identityBob = Identity.generate();
|
||||
|
||||
dbg("Create messengers");
|
||||
|
||||
[messengerAlice, messengerBob] = await Promise.all([
|
||||
Messenger.create(identityAlice, { bootstrap: {} }),
|
||||
Messenger.create(identityBob, {
|
||||
bootstrap: {},
|
||||
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } },
|
||||
}),
|
||||
]);
|
||||
|
||||
dbg("Connect messengers");
|
||||
// Connect both messengers together for test purposes
|
||||
messengerAlice.waku.addPeerToAddressBook(
|
||||
messengerBob.waku.libp2p.peerId,
|
||||
messengerBob.waku.libp2p.multiaddrs
|
||||
);
|
||||
|
||||
dbg("Wait for remote peer");
|
||||
await Promise.all([
|
||||
messengerAlice.waku.waitForRemotePeer([Protocols.Relay]),
|
||||
messengerBob.waku.waitForRemotePeer([Protocols.Relay]),
|
||||
]);
|
||||
dbg("Messengers ready");
|
||||
});
|
||||
|
||||
it("Sends & Receive public chat messages", async function () {
|
||||
this.timeout(10_000);
|
||||
|
||||
await messengerAlice.joinChatById(testChatId);
|
||||
await messengerBob.joinChatById(testChatId);
|
||||
|
||||
const text = "This is a message.";
|
||||
|
||||
const receivedMessagePromise: Promise<ApplicationMetadataMessage> =
|
||||
new Promise((resolve) => {
|
||||
messengerBob.addObserver((message) => {
|
||||
resolve(message);
|
||||
}, testChatId);
|
||||
});
|
||||
|
||||
await messengerAlice.sendMessage(testChatId, {
|
||||
text,
|
||||
contentType: ContentType.Text,
|
||||
});
|
||||
|
||||
const receivedMessage = await receivedMessagePromise;
|
||||
|
||||
expect(receivedMessage.chatMessage?.text).to.eq(text);
|
||||
});
|
||||
|
||||
it("public chat messages have signers", async function () {
|
||||
this.timeout(10_000);
|
||||
|
||||
await messengerAlice.joinChatById(testChatId);
|
||||
await messengerBob.joinChatById(testChatId);
|
||||
|
||||
const text = "This is a message.";
|
||||
|
||||
const receivedMessagePromise: Promise<ApplicationMetadataMessage> =
|
||||
new Promise((resolve) => {
|
||||
messengerBob.addObserver((message) => {
|
||||
resolve(message);
|
||||
}, testChatId);
|
||||
});
|
||||
|
||||
await messengerAlice.sendMessage(testChatId, {
|
||||
text,
|
||||
contentType: ContentType.Text,
|
||||
});
|
||||
|
||||
const receivedMessage = await receivedMessagePromise;
|
||||
|
||||
expect(bufToHex(receivedMessage.signer!)).to.eq(
|
||||
bufToHex(identityAlice.publicKey)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
this.timeout(5000);
|
||||
await messengerAlice.stop();
|
||||
await messengerBob.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Messenger [live data]", () => {
|
||||
before(function () {
|
||||
if (process.env.CI) {
|
||||
// Skip live data test in CI
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
let messenger: Messenger;
|
||||
let identity: Identity;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.timeout(20_000);
|
||||
|
||||
dbg("Generate keys");
|
||||
identity = Identity.generate();
|
||||
|
||||
dbg("Create messengers");
|
||||
|
||||
messenger = await Messenger.create(identity, {
|
||||
bootstrap: { default: true },
|
||||
});
|
||||
|
||||
dbg("Wait to be connected to a peer");
|
||||
await messenger.waku.waitForRemotePeer();
|
||||
dbg("Messengers ready");
|
||||
});
|
||||
|
||||
it("Receive public chat messages", async function () {
|
||||
this.timeout(20_000);
|
||||
|
||||
const community = await Community.instantiateCommunity(
|
||||
"0x02cf13719c8b836bebd4e430c497ee38e798a43e4d8c4760c34bbd9bf4f2434d26",
|
||||
messenger.waku
|
||||
);
|
||||
|
||||
await messenger.joinChats(community.chats.values());
|
||||
|
||||
const startTime = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const endTime = new Date();
|
||||
|
||||
const chat = Array.from(community.chats.values()).find(
|
||||
(chat) => chat.communityChat?.identity?.displayName === "Test Chat"
|
||||
);
|
||||
|
||||
if (!chat) throw "Could not find foobar chat";
|
||||
|
||||
console.log(chat);
|
||||
|
||||
await messenger.retrievePreviousMessages(
|
||||
chat.id,
|
||||
startTime,
|
||||
endTime,
|
||||
(metadata) => {
|
||||
metadata.forEach((m) => {
|
||||
console.log("Message", m.chatMessage?.text);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
this.timeout(5000);
|
||||
await messenger.stop();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,268 @@
|
|||
import debug from "debug";
|
||||
import { Waku, WakuMessage } from "js-waku";
|
||||
import { CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku";
|
||||
import { DecryptionMethod } from "js-waku/build/main/lib/waku_message";
|
||||
|
||||
import { Chat } from "./chat";
|
||||
import { Identity } from "./identity";
|
||||
import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message";
|
||||
import { getLatestUserNickname } from "./utils";
|
||||
import { ApplicationMetadataMessage } from "./wire/application_metadata_message";
|
||||
import { ChatMessage, Content } from "./wire/chat_message";
|
||||
|
||||
const dbg = debug("communities:messenger");
|
||||
|
||||
export class Messenger {
|
||||
waku: Waku;
|
||||
chatsById: Map<string, Chat>;
|
||||
observers: {
|
||||
[chatId: string]: Set<
|
||||
(
|
||||
message: ApplicationMetadataMessage,
|
||||
timestamp: Date,
|
||||
chatId: string
|
||||
) => void
|
||||
>;
|
||||
};
|
||||
identity: Identity | undefined;
|
||||
|
||||
private constructor(identity: Identity | undefined, waku: Waku) {
|
||||
this.identity = identity;
|
||||
this.waku = waku;
|
||||
this.chatsById = new Map();
|
||||
this.observers = {};
|
||||
}
|
||||
|
||||
public static async create(
|
||||
identity: Identity | undefined,
|
||||
wakuOptions?: WakuCreateOptions
|
||||
): Promise<Messenger> {
|
||||
const _wakuOptions = Object.assign(
|
||||
{ bootstrap: { default: true } },
|
||||
wakuOptions
|
||||
);
|
||||
const waku = await Waku.create(_wakuOptions);
|
||||
return new Messenger(identity, waku);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a public chat using its id.
|
||||
*
|
||||
* For community chats, prefer [[joinChat]].
|
||||
*
|
||||
* Use `addListener` to get messages received on this chat.
|
||||
*/
|
||||
public async joinChatById(chatId: string): Promise<void> {
|
||||
const chat = await Chat.create(chatId);
|
||||
|
||||
await this.joinChat(chat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins several of public chats.
|
||||
*
|
||||
* Use `addListener` to get messages received on these chats.
|
||||
*/
|
||||
public async joinChats(chats: Iterable<Chat>): Promise<void> {
|
||||
await Promise.all(
|
||||
Array.from(chats).map((chat) => {
|
||||
return this.joinChat(chat);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a public chat.
|
||||
*
|
||||
* Use `addListener` to get messages received on this chat.
|
||||
*/
|
||||
public async joinChat(chat: Chat): Promise<void> {
|
||||
if (this.chatsById.has(chat.id))
|
||||
throw `Failed to join chat, it is already joined: ${chat.id}`;
|
||||
|
||||
this.waku.addDecryptionKey(chat.symKey, {
|
||||
method: DecryptionMethod.Symmetric,
|
||||
contentTopics: [chat.contentTopic],
|
||||
});
|
||||
|
||||
this.waku.relay.addObserver(
|
||||
(wakuMessage: WakuMessage) => {
|
||||
if (!wakuMessage.payload || !wakuMessage.timestamp) return;
|
||||
|
||||
const message = ApplicationMetadataMessage.decode(wakuMessage.payload);
|
||||
|
||||
switch (message.type) {
|
||||
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
|
||||
this._handleNewChatMessage(chat, message, wakuMessage.timestamp);
|
||||
break;
|
||||
default:
|
||||
dbg("Received unsupported message type", message.type);
|
||||
}
|
||||
},
|
||||
[chat.contentTopic]
|
||||
);
|
||||
|
||||
this.chatsById.set(chat.id, chat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message on the given chat Id.
|
||||
*/
|
||||
public async sendMessage(
|
||||
chatId: string,
|
||||
content: Content,
|
||||
responseTo?: string
|
||||
): Promise<void> {
|
||||
if (this.identity) {
|
||||
const chat = this.chatsById.get(chatId);
|
||||
if (!chat) throw `Failed to send message, chat not joined: ${chatId}`;
|
||||
|
||||
const chatMessage = chat.createMessage(content, responseTo);
|
||||
|
||||
const appMetadataMessage = ApplicationMetadataMessage.create(
|
||||
chatMessage.encode(),
|
||||
ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE,
|
||||
this.identity
|
||||
);
|
||||
|
||||
const wakuMessage = await WakuMessage.fromBytes(
|
||||
appMetadataMessage.encode(),
|
||||
chat.contentTopic,
|
||||
{ symKey: chat.symKey, sigPrivKey: this.identity.privateKey }
|
||||
);
|
||||
|
||||
await this.waku.relay.send(wakuMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an observer of new messages received on the given chat id.
|
||||
*
|
||||
* @throws string If the chat has not been joined first using [joinChat].
|
||||
*/
|
||||
public addObserver(
|
||||
observer: (
|
||||
message: ApplicationMetadataMessage,
|
||||
timestamp: Date,
|
||||
chatId: string
|
||||
) => void,
|
||||
chatId: string | string[]
|
||||
): void {
|
||||
let chats = [];
|
||||
|
||||
if (typeof chatId === "string") {
|
||||
chats.push(chatId);
|
||||
} else {
|
||||
chats = [...chatId];
|
||||
}
|
||||
|
||||
chats.forEach((id) => {
|
||||
if (!this.chatsById.has(id))
|
||||
throw "Cannot add observer on a chat that is not joined.";
|
||||
if (!this.observers[id]) {
|
||||
this.observers[id] = new Set();
|
||||
}
|
||||
|
||||
this.observers[id].add(observer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an observer of new messages received on the given chat id.
|
||||
*
|
||||
* @throws string If the chat has not been joined first using [joinChat].
|
||||
*/
|
||||
|
||||
deleteObserver(
|
||||
observer: (message: ApplicationMetadataMessage) => void,
|
||||
chatId: string
|
||||
): void {
|
||||
if (this.observers[chatId]) {
|
||||
this.observers[chatId].delete(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the messenger.
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
await this.waku.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve previous messages from a Waku Store node for the given chat Id.
|
||||
*
|
||||
* Note: note sure what is the preferred interface: callback or returning all messages
|
||||
* Callback is more flexible and allow processing messages as they are retrieved instead of waiting for the
|
||||
* full retrieval via paging to be done.
|
||||
*/
|
||||
public async retrievePreviousMessages(
|
||||
chatId: string,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
callback?: (messages: ApplicationMetadataMessage[]) => void
|
||||
): Promise<number> {
|
||||
const chat = this.chatsById.get(chatId);
|
||||
if (!chat)
|
||||
throw `Failed to retrieve messages, chat is not joined: ${chatId}`;
|
||||
|
||||
const _callback = (wakuMessages: WakuMessage[]): void => {
|
||||
const isDefined = (
|
||||
msg: ApplicationMetadataMessage | undefined
|
||||
): msg is ApplicationMetadataMessage => {
|
||||
return !!msg;
|
||||
};
|
||||
|
||||
const messages = wakuMessages.map((wakuMessage: WakuMessage) => {
|
||||
if (!wakuMessage.payload || !wakuMessage.timestamp) return;
|
||||
|
||||
const message = ApplicationMetadataMessage.decode(wakuMessage.payload);
|
||||
|
||||
switch (message.type) {
|
||||
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
|
||||
this._handleNewChatMessage(chat, message, wakuMessage.timestamp);
|
||||
return message;
|
||||
default:
|
||||
dbg("Retrieved unsupported message type", message.type);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (callback) {
|
||||
callback(messages.filter(isDefined));
|
||||
}
|
||||
};
|
||||
const allMessages = await this.waku.store.queryHistory(
|
||||
[chat.contentTopic],
|
||||
{
|
||||
timeFilter: { startTime, endTime },
|
||||
callback: _callback,
|
||||
}
|
||||
);
|
||||
return allMessages.length;
|
||||
}
|
||||
|
||||
private _handleNewChatMessage(
|
||||
chat: Chat,
|
||||
message: ApplicationMetadataMessage,
|
||||
timestamp: Date
|
||||
): void {
|
||||
if (!message.payload || !message.type || !message.signature) return;
|
||||
|
||||
const chatMessage = ChatMessage.decode(message.payload);
|
||||
chat.handleNewMessage(chatMessage);
|
||||
|
||||
if (this.observers[chat.id]) {
|
||||
this.observers[chat.id].forEach((observer) => {
|
||||
observer(message, timestamp, chat.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkIfUserInWakuNetwork(publicKey: Uint8Array): Promise<boolean> {
|
||||
const { clock, nickname } = await getLatestUserNickname(
|
||||
publicKey,
|
||||
this.waku
|
||||
);
|
||||
return clock > 0 && nickname !== "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,532 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
import {
|
||||
ImageType,
|
||||
imageTypeFromJSON,
|
||||
imageTypeToJSON,
|
||||
} from "../../communities/v1/enums";
|
||||
|
||||
export const protobufPackage = "communities.v1";
|
||||
|
||||
/** ChatIdentity represents the user defined identity associated with their public chat key */
|
||||
export interface ChatIdentity {
|
||||
/** Lamport timestamp of the message */
|
||||
clock: number;
|
||||
/** ens_name is the valid ENS name associated with the chat key */
|
||||
ensName: string;
|
||||
/** images is a string indexed mapping of images associated with an identity */
|
||||
images: { [key: string]: IdentityImage };
|
||||
/** display name is the user set identity, valid only for organisations */
|
||||
displayName: string;
|
||||
/** description is the user set description, valid only for organisations */
|
||||
description: string;
|
||||
color: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
export interface ChatIdentity_ImagesEntry {
|
||||
key: string;
|
||||
value: IdentityImage | undefined;
|
||||
}
|
||||
|
||||
/** ProfileImage represents data associated with a user's profile image */
|
||||
export interface IdentityImage {
|
||||
/**
|
||||
* payload is a context based payload for the profile image data,
|
||||
* context is determined by the `source_type`
|
||||
*/
|
||||
payload: Uint8Array;
|
||||
/** source_type signals the image payload source */
|
||||
sourceType: IdentityImage_SourceType;
|
||||
/** image_type signals the image type and method of parsing the payload */
|
||||
imageType: ImageType;
|
||||
}
|
||||
|
||||
/** SourceType are the predefined types of image source allowed */
|
||||
export enum IdentityImage_SourceType {
|
||||
UNKNOWN_SOURCE_TYPE = 0,
|
||||
/** RAW_PAYLOAD - RAW_PAYLOAD image byte data */
|
||||
RAW_PAYLOAD = 1,
|
||||
/**
|
||||
* ENS_AVATAR - ENS_AVATAR uses the ENS record's resolver get-text-data.avatar data
|
||||
* The `payload` field will be ignored if ENS_AVATAR is selected
|
||||
* The application will read and parse the ENS avatar data as image payload data, URLs will be ignored
|
||||
* The parent `ChatMessageIdentity` must have a valid `ens_name` set
|
||||
*/
|
||||
ENS_AVATAR = 2,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function identityImage_SourceTypeFromJSON(
|
||||
object: any
|
||||
): IdentityImage_SourceType {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "UNKNOWN_SOURCE_TYPE":
|
||||
return IdentityImage_SourceType.UNKNOWN_SOURCE_TYPE;
|
||||
case 1:
|
||||
case "RAW_PAYLOAD":
|
||||
return IdentityImage_SourceType.RAW_PAYLOAD;
|
||||
case 2:
|
||||
case "ENS_AVATAR":
|
||||
return IdentityImage_SourceType.ENS_AVATAR;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return IdentityImage_SourceType.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function identityImage_SourceTypeToJSON(
|
||||
object: IdentityImage_SourceType
|
||||
): string {
|
||||
switch (object) {
|
||||
case IdentityImage_SourceType.UNKNOWN_SOURCE_TYPE:
|
||||
return "UNKNOWN_SOURCE_TYPE";
|
||||
case IdentityImage_SourceType.RAW_PAYLOAD:
|
||||
return "RAW_PAYLOAD";
|
||||
case IdentityImage_SourceType.ENS_AVATAR:
|
||||
return "ENS_AVATAR";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const baseChatIdentity: object = {
|
||||
clock: 0,
|
||||
ensName: "",
|
||||
displayName: "",
|
||||
description: "",
|
||||
color: "",
|
||||
emoji: "",
|
||||
};
|
||||
|
||||
export const ChatIdentity = {
|
||||
encode(
|
||||
message: ChatIdentity,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.clock !== 0) {
|
||||
writer.uint32(8).uint64(message.clock);
|
||||
}
|
||||
if (message.ensName !== "") {
|
||||
writer.uint32(18).string(message.ensName);
|
||||
}
|
||||
Object.entries(message.images).forEach(([key, value]) => {
|
||||
ChatIdentity_ImagesEntry.encode(
|
||||
{ key: key as any, value },
|
||||
writer.uint32(26).fork()
|
||||
).ldelim();
|
||||
});
|
||||
if (message.displayName !== "") {
|
||||
writer.uint32(34).string(message.displayName);
|
||||
}
|
||||
if (message.description !== "") {
|
||||
writer.uint32(42).string(message.description);
|
||||
}
|
||||
if (message.color !== "") {
|
||||
writer.uint32(50).string(message.color);
|
||||
}
|
||||
if (message.emoji !== "") {
|
||||
writer.uint32(58).string(message.emoji);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): ChatIdentity {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = { ...baseChatIdentity } as ChatIdentity;
|
||||
message.images = {};
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.clock = longToNumber(reader.uint64() as Long);
|
||||
break;
|
||||
case 2:
|
||||
message.ensName = reader.string();
|
||||
break;
|
||||
case 3:
|
||||
const entry3 = ChatIdentity_ImagesEntry.decode(
|
||||
reader,
|
||||
reader.uint32()
|
||||
);
|
||||
if (entry3.value !== undefined) {
|
||||
message.images[entry3.key] = entry3.value;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
message.displayName = reader.string();
|
||||
break;
|
||||
case 5:
|
||||
message.description = reader.string();
|
||||
break;
|
||||
case 6:
|
||||
message.color = reader.string();
|
||||
break;
|
||||
case 7:
|
||||
message.emoji = reader.string();
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ChatIdentity {
|
||||
const message = { ...baseChatIdentity } as ChatIdentity;
|
||||
message.images = {};
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = Number(object.clock);
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.ensName !== undefined && object.ensName !== null) {
|
||||
message.ensName = String(object.ensName);
|
||||
} else {
|
||||
message.ensName = "";
|
||||
}
|
||||
if (object.images !== undefined && object.images !== null) {
|
||||
Object.entries(object.images).forEach(([key, value]) => {
|
||||
message.images[key] = IdentityImage.fromJSON(value);
|
||||
});
|
||||
}
|
||||
if (object.displayName !== undefined && object.displayName !== null) {
|
||||
message.displayName = String(object.displayName);
|
||||
} else {
|
||||
message.displayName = "";
|
||||
}
|
||||
if (object.description !== undefined && object.description !== null) {
|
||||
message.description = String(object.description);
|
||||
} else {
|
||||
message.description = "";
|
||||
}
|
||||
if (object.color !== undefined && object.color !== null) {
|
||||
message.color = String(object.color);
|
||||
} else {
|
||||
message.color = "";
|
||||
}
|
||||
if (object.emoji !== undefined && object.emoji !== null) {
|
||||
message.emoji = String(object.emoji);
|
||||
} else {
|
||||
message.emoji = "";
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: ChatIdentity): unknown {
|
||||
const obj: any = {};
|
||||
message.clock !== undefined && (obj.clock = message.clock);
|
||||
message.ensName !== undefined && (obj.ensName = message.ensName);
|
||||
obj.images = {};
|
||||
if (message.images) {
|
||||
Object.entries(message.images).forEach(([k, v]) => {
|
||||
obj.images[k] = IdentityImage.toJSON(v);
|
||||
});
|
||||
}
|
||||
message.displayName !== undefined &&
|
||||
(obj.displayName = message.displayName);
|
||||
message.description !== undefined &&
|
||||
(obj.description = message.description);
|
||||
message.color !== undefined && (obj.color = message.color);
|
||||
message.emoji !== undefined && (obj.emoji = message.emoji);
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(object: DeepPartial<ChatIdentity>): ChatIdentity {
|
||||
const message = { ...baseChatIdentity } as ChatIdentity;
|
||||
message.images = {};
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = object.clock;
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.ensName !== undefined && object.ensName !== null) {
|
||||
message.ensName = object.ensName;
|
||||
} else {
|
||||
message.ensName = "";
|
||||
}
|
||||
if (object.images !== undefined && object.images !== null) {
|
||||
Object.entries(object.images).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
message.images[key] = IdentityImage.fromPartial(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (object.displayName !== undefined && object.displayName !== null) {
|
||||
message.displayName = object.displayName;
|
||||
} else {
|
||||
message.displayName = "";
|
||||
}
|
||||
if (object.description !== undefined && object.description !== null) {
|
||||
message.description = object.description;
|
||||
} else {
|
||||
message.description = "";
|
||||
}
|
||||
if (object.color !== undefined && object.color !== null) {
|
||||
message.color = object.color;
|
||||
} else {
|
||||
message.color = "";
|
||||
}
|
||||
if (object.emoji !== undefined && object.emoji !== null) {
|
||||
message.emoji = object.emoji;
|
||||
} else {
|
||||
message.emoji = "";
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
const baseChatIdentity_ImagesEntry: object = { key: "" };
|
||||
|
||||
export const ChatIdentity_ImagesEntry = {
|
||||
encode(
|
||||
message: ChatIdentity_ImagesEntry,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.key !== "") {
|
||||
writer.uint32(10).string(message.key);
|
||||
}
|
||||
if (message.value !== undefined) {
|
||||
IdentityImage.encode(message.value, writer.uint32(18).fork()).ldelim();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(
|
||||
input: _m0.Reader | Uint8Array,
|
||||
length?: number
|
||||
): ChatIdentity_ImagesEntry {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = {
|
||||
...baseChatIdentity_ImagesEntry,
|
||||
} as ChatIdentity_ImagesEntry;
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.key = reader.string();
|
||||
break;
|
||||
case 2:
|
||||
message.value = IdentityImage.decode(reader, reader.uint32());
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ChatIdentity_ImagesEntry {
|
||||
const message = {
|
||||
...baseChatIdentity_ImagesEntry,
|
||||
} as ChatIdentity_ImagesEntry;
|
||||
if (object.key !== undefined && object.key !== null) {
|
||||
message.key = String(object.key);
|
||||
} else {
|
||||
message.key = "";
|
||||
}
|
||||
if (object.value !== undefined && object.value !== null) {
|
||||
message.value = IdentityImage.fromJSON(object.value);
|
||||
} else {
|
||||
message.value = undefined;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: ChatIdentity_ImagesEntry): unknown {
|
||||
const obj: any = {};
|
||||
message.key !== undefined && (obj.key = message.key);
|
||||
message.value !== undefined &&
|
||||
(obj.value = message.value
|
||||
? IdentityImage.toJSON(message.value)
|
||||
: undefined);
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(
|
||||
object: DeepPartial<ChatIdentity_ImagesEntry>
|
||||
): ChatIdentity_ImagesEntry {
|
||||
const message = {
|
||||
...baseChatIdentity_ImagesEntry,
|
||||
} as ChatIdentity_ImagesEntry;
|
||||
if (object.key !== undefined && object.key !== null) {
|
||||
message.key = object.key;
|
||||
} else {
|
||||
message.key = "";
|
||||
}
|
||||
if (object.value !== undefined && object.value !== null) {
|
||||
message.value = IdentityImage.fromPartial(object.value);
|
||||
} else {
|
||||
message.value = undefined;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
const baseIdentityImage: object = { sourceType: 0, imageType: 0 };
|
||||
|
||||
export const IdentityImage = {
|
||||
encode(
|
||||
message: IdentityImage,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.payload.length !== 0) {
|
||||
writer.uint32(10).bytes(message.payload);
|
||||
}
|
||||
if (message.sourceType !== 0) {
|
||||
writer.uint32(16).int32(message.sourceType);
|
||||
}
|
||||
if (message.imageType !== 0) {
|
||||
writer.uint32(24).int32(message.imageType);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): IdentityImage {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = { ...baseIdentityImage } as IdentityImage;
|
||||
message.payload = new Uint8Array();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.payload = reader.bytes();
|
||||
break;
|
||||
case 2:
|
||||
message.sourceType = reader.int32() as any;
|
||||
break;
|
||||
case 3:
|
||||
message.imageType = reader.int32() as any;
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): IdentityImage {
|
||||
const message = { ...baseIdentityImage } as IdentityImage;
|
||||
message.payload = new Uint8Array();
|
||||
if (object.payload !== undefined && object.payload !== null) {
|
||||
message.payload = bytesFromBase64(object.payload);
|
||||
}
|
||||
if (object.sourceType !== undefined && object.sourceType !== null) {
|
||||
message.sourceType = identityImage_SourceTypeFromJSON(object.sourceType);
|
||||
} else {
|
||||
message.sourceType = 0;
|
||||
}
|
||||
if (object.imageType !== undefined && object.imageType !== null) {
|
||||
message.imageType = imageTypeFromJSON(object.imageType);
|
||||
} else {
|
||||
message.imageType = 0;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: IdentityImage): unknown {
|
||||
const obj: any = {};
|
||||
message.payload !== undefined &&
|
||||
(obj.payload = base64FromBytes(
|
||||
message.payload !== undefined ? message.payload : new Uint8Array()
|
||||
));
|
||||
message.sourceType !== undefined &&
|
||||
(obj.sourceType = identityImage_SourceTypeToJSON(message.sourceType));
|
||||
message.imageType !== undefined &&
|
||||
(obj.imageType = imageTypeToJSON(message.imageType));
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(object: DeepPartial<IdentityImage>): IdentityImage {
|
||||
const message = { ...baseIdentityImage } as IdentityImage;
|
||||
if (object.payload !== undefined && object.payload !== null) {
|
||||
message.payload = object.payload;
|
||||
} else {
|
||||
message.payload = new Uint8Array();
|
||||
}
|
||||
if (object.sourceType !== undefined && object.sourceType !== null) {
|
||||
message.sourceType = object.sourceType;
|
||||
} else {
|
||||
message.sourceType = 0;
|
||||
}
|
||||
if (object.imageType !== undefined && object.imageType !== null) {
|
||||
message.imageType = object.imageType;
|
||||
} else {
|
||||
message.imageType = 0;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
declare var self: any | undefined;
|
||||
declare var window: any | undefined;
|
||||
declare var global: any | undefined;
|
||||
var globalThis: any = (() => {
|
||||
if (typeof globalThis !== "undefined") return globalThis;
|
||||
if (typeof self !== "undefined") return self;
|
||||
if (typeof window !== "undefined") return window;
|
||||
if (typeof global !== "undefined") return global;
|
||||
throw "Unable to locate global object";
|
||||
})();
|
||||
|
||||
const atob: (b64: string) => string =
|
||||
globalThis.atob ||
|
||||
((b64) => globalThis.Buffer.from(b64, "base64").toString("binary"));
|
||||
function bytesFromBase64(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; ++i) {
|
||||
arr[i] = bin.charCodeAt(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const btoa: (bin: string) => string =
|
||||
globalThis.btoa ||
|
||||
((bin) => globalThis.Buffer.from(bin, "binary").toString("base64"));
|
||||
function base64FromBytes(arr: Uint8Array): string {
|
||||
const bin: string[] = [];
|
||||
for (const byte of arr) {
|
||||
bin.push(String.fromCharCode(byte));
|
||||
}
|
||||
return btoa(bin.join(""));
|
||||
}
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
function longToNumber(long: Long): number {
|
||||
if (long.gt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
return long.toNumber();
|
||||
}
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,328 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
import {
|
||||
MessageType,
|
||||
messageTypeFromJSON,
|
||||
messageTypeToJSON,
|
||||
} from "../../communities/v1/enums";
|
||||
|
||||
export const protobufPackage = "communities.v1";
|
||||
|
||||
export interface EmojiReaction {
|
||||
/** clock Lamport timestamp of the chat message */
|
||||
clock: number;
|
||||
/**
|
||||
* chat_id the ID of the chat the message belongs to, for query efficiency the chat_id is stored in the db even though the
|
||||
* target message also stores the chat_id
|
||||
*/
|
||||
chatId: string;
|
||||
/** message_id the ID of the target message that the user wishes to react to */
|
||||
messageId: string;
|
||||
/** message_type is (somewhat confusingly) the ID of the type of chat the message belongs to */
|
||||
messageType: MessageType;
|
||||
/** type the ID of the emoji the user wishes to react with */
|
||||
type: EmojiReaction_Type;
|
||||
/** whether this is a rectraction of a previously sent emoji */
|
||||
retracted: boolean;
|
||||
/** Grant for organisation chat messages */
|
||||
grant: Uint8Array;
|
||||
}
|
||||
|
||||
export enum EmojiReaction_Type {
|
||||
UNKNOWN_EMOJI_REACTION_TYPE = 0,
|
||||
LOVE = 1,
|
||||
THUMBS_UP = 2,
|
||||
THUMBS_DOWN = 3,
|
||||
LAUGH = 4,
|
||||
SAD = 5,
|
||||
ANGRY = 6,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function emojiReaction_TypeFromJSON(object: any): EmojiReaction_Type {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "UNKNOWN_EMOJI_REACTION_TYPE":
|
||||
return EmojiReaction_Type.UNKNOWN_EMOJI_REACTION_TYPE;
|
||||
case 1:
|
||||
case "LOVE":
|
||||
return EmojiReaction_Type.LOVE;
|
||||
case 2:
|
||||
case "THUMBS_UP":
|
||||
return EmojiReaction_Type.THUMBS_UP;
|
||||
case 3:
|
||||
case "THUMBS_DOWN":
|
||||
return EmojiReaction_Type.THUMBS_DOWN;
|
||||
case 4:
|
||||
case "LAUGH":
|
||||
return EmojiReaction_Type.LAUGH;
|
||||
case 5:
|
||||
case "SAD":
|
||||
return EmojiReaction_Type.SAD;
|
||||
case 6:
|
||||
case "ANGRY":
|
||||
return EmojiReaction_Type.ANGRY;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return EmojiReaction_Type.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function emojiReaction_TypeToJSON(object: EmojiReaction_Type): string {
|
||||
switch (object) {
|
||||
case EmojiReaction_Type.UNKNOWN_EMOJI_REACTION_TYPE:
|
||||
return "UNKNOWN_EMOJI_REACTION_TYPE";
|
||||
case EmojiReaction_Type.LOVE:
|
||||
return "LOVE";
|
||||
case EmojiReaction_Type.THUMBS_UP:
|
||||
return "THUMBS_UP";
|
||||
case EmojiReaction_Type.THUMBS_DOWN:
|
||||
return "THUMBS_DOWN";
|
||||
case EmojiReaction_Type.LAUGH:
|
||||
return "LAUGH";
|
||||
case EmojiReaction_Type.SAD:
|
||||
return "SAD";
|
||||
case EmojiReaction_Type.ANGRY:
|
||||
return "ANGRY";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const baseEmojiReaction: object = {
|
||||
clock: 0,
|
||||
chatId: "",
|
||||
messageId: "",
|
||||
messageType: 0,
|
||||
type: 0,
|
||||
retracted: false,
|
||||
};
|
||||
|
||||
export const EmojiReaction = {
|
||||
encode(
|
||||
message: EmojiReaction,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.clock !== 0) {
|
||||
writer.uint32(8).uint64(message.clock);
|
||||
}
|
||||
if (message.chatId !== "") {
|
||||
writer.uint32(18).string(message.chatId);
|
||||
}
|
||||
if (message.messageId !== "") {
|
||||
writer.uint32(26).string(message.messageId);
|
||||
}
|
||||
if (message.messageType !== 0) {
|
||||
writer.uint32(32).int32(message.messageType);
|
||||
}
|
||||
if (message.type !== 0) {
|
||||
writer.uint32(40).int32(message.type);
|
||||
}
|
||||
if (message.retracted === true) {
|
||||
writer.uint32(48).bool(message.retracted);
|
||||
}
|
||||
if (message.grant.length !== 0) {
|
||||
writer.uint32(58).bytes(message.grant);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): EmojiReaction {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = { ...baseEmojiReaction } as EmojiReaction;
|
||||
message.grant = new Uint8Array();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.clock = longToNumber(reader.uint64() as Long);
|
||||
break;
|
||||
case 2:
|
||||
message.chatId = reader.string();
|
||||
break;
|
||||
case 3:
|
||||
message.messageId = reader.string();
|
||||
break;
|
||||
case 4:
|
||||
message.messageType = reader.int32() as any;
|
||||
break;
|
||||
case 5:
|
||||
message.type = reader.int32() as any;
|
||||
break;
|
||||
case 6:
|
||||
message.retracted = reader.bool();
|
||||
break;
|
||||
case 7:
|
||||
message.grant = reader.bytes();
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): EmojiReaction {
|
||||
const message = { ...baseEmojiReaction } as EmojiReaction;
|
||||
message.grant = new Uint8Array();
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = Number(object.clock);
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.chatId !== undefined && object.chatId !== null) {
|
||||
message.chatId = String(object.chatId);
|
||||
} else {
|
||||
message.chatId = "";
|
||||
}
|
||||
if (object.messageId !== undefined && object.messageId !== null) {
|
||||
message.messageId = String(object.messageId);
|
||||
} else {
|
||||
message.messageId = "";
|
||||
}
|
||||
if (object.messageType !== undefined && object.messageType !== null) {
|
||||
message.messageType = messageTypeFromJSON(object.messageType);
|
||||
} else {
|
||||
message.messageType = 0;
|
||||
}
|
||||
if (object.type !== undefined && object.type !== null) {
|
||||
message.type = emojiReaction_TypeFromJSON(object.type);
|
||||
} else {
|
||||
message.type = 0;
|
||||
}
|
||||
if (object.retracted !== undefined && object.retracted !== null) {
|
||||
message.retracted = Boolean(object.retracted);
|
||||
} else {
|
||||
message.retracted = false;
|
||||
}
|
||||
if (object.grant !== undefined && object.grant !== null) {
|
||||
message.grant = bytesFromBase64(object.grant);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: EmojiReaction): unknown {
|
||||
const obj: any = {};
|
||||
message.clock !== undefined && (obj.clock = message.clock);
|
||||
message.chatId !== undefined && (obj.chatId = message.chatId);
|
||||
message.messageId !== undefined && (obj.messageId = message.messageId);
|
||||
message.messageType !== undefined &&
|
||||
(obj.messageType = messageTypeToJSON(message.messageType));
|
||||
message.type !== undefined &&
|
||||
(obj.type = emojiReaction_TypeToJSON(message.type));
|
||||
message.retracted !== undefined && (obj.retracted = message.retracted);
|
||||
message.grant !== undefined &&
|
||||
(obj.grant = base64FromBytes(
|
||||
message.grant !== undefined ? message.grant : new Uint8Array()
|
||||
));
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(object: DeepPartial<EmojiReaction>): EmojiReaction {
|
||||
const message = { ...baseEmojiReaction } as EmojiReaction;
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = object.clock;
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.chatId !== undefined && object.chatId !== null) {
|
||||
message.chatId = object.chatId;
|
||||
} else {
|
||||
message.chatId = "";
|
||||
}
|
||||
if (object.messageId !== undefined && object.messageId !== null) {
|
||||
message.messageId = object.messageId;
|
||||
} else {
|
||||
message.messageId = "";
|
||||
}
|
||||
if (object.messageType !== undefined && object.messageType !== null) {
|
||||
message.messageType = object.messageType;
|
||||
} else {
|
||||
message.messageType = 0;
|
||||
}
|
||||
if (object.type !== undefined && object.type !== null) {
|
||||
message.type = object.type;
|
||||
} else {
|
||||
message.type = 0;
|
||||
}
|
||||
if (object.retracted !== undefined && object.retracted !== null) {
|
||||
message.retracted = object.retracted;
|
||||
} else {
|
||||
message.retracted = false;
|
||||
}
|
||||
if (object.grant !== undefined && object.grant !== null) {
|
||||
message.grant = object.grant;
|
||||
} else {
|
||||
message.grant = new Uint8Array();
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
declare var self: any | undefined;
|
||||
declare var window: any | undefined;
|
||||
declare var global: any | undefined;
|
||||
var globalThis: any = (() => {
|
||||
if (typeof globalThis !== "undefined") return globalThis;
|
||||
if (typeof self !== "undefined") return self;
|
||||
if (typeof window !== "undefined") return window;
|
||||
if (typeof global !== "undefined") return global;
|
||||
throw "Unable to locate global object";
|
||||
})();
|
||||
|
||||
const atob: (b64: string) => string =
|
||||
globalThis.atob ||
|
||||
((b64) => globalThis.Buffer.from(b64, "base64").toString("binary"));
|
||||
function bytesFromBase64(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; ++i) {
|
||||
arr[i] = bin.charCodeAt(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const btoa: (bin: string) => string =
|
||||
globalThis.btoa ||
|
||||
((bin) => globalThis.Buffer.from(bin, "binary").toString("base64"));
|
||||
function base64FromBytes(arr: Uint8Array): string {
|
||||
const bin: string[] = [];
|
||||
for (const byte of arr) {
|
||||
bin.push(String.fromCharCode(byte));
|
||||
}
|
||||
return btoa(bin.join(""));
|
||||
}
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
function longToNumber(long: Long): number {
|
||||
if (long.gt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
return long.toNumber();
|
||||
}
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "communities.v1";
|
||||
|
||||
export enum MessageType {
|
||||
MESSAGE_TYPE_UNKNOWN_UNSPECIFIED = 0,
|
||||
MESSAGE_TYPE_ONE_TO_ONE = 1,
|
||||
MESSAGE_TYPE_MESSAGE_TYPE_PUBLIC_GROUP = 2,
|
||||
MESSAGE_TYPE_PRIVATE_GROUP = 3,
|
||||
/** MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP - Only local */
|
||||
MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP = 4,
|
||||
MESSAGE_TYPE_COMMUNITY_CHAT = 5,
|
||||
/** MESSAGE_TYPE_SYSTEM_MESSAGE_GAP - Only local */
|
||||
MESSAGE_TYPE_SYSTEM_MESSAGE_GAP = 6,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function messageTypeFromJSON(object: any): MessageType {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "MESSAGE_TYPE_UNKNOWN_UNSPECIFIED":
|
||||
return MessageType.MESSAGE_TYPE_UNKNOWN_UNSPECIFIED;
|
||||
case 1:
|
||||
case "MESSAGE_TYPE_ONE_TO_ONE":
|
||||
return MessageType.MESSAGE_TYPE_ONE_TO_ONE;
|
||||
case 2:
|
||||
case "MESSAGE_TYPE_MESSAGE_TYPE_PUBLIC_GROUP":
|
||||
return MessageType.MESSAGE_TYPE_MESSAGE_TYPE_PUBLIC_GROUP;
|
||||
case 3:
|
||||
case "MESSAGE_TYPE_PRIVATE_GROUP":
|
||||
return MessageType.MESSAGE_TYPE_PRIVATE_GROUP;
|
||||
case 4:
|
||||
case "MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP":
|
||||
return MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP;
|
||||
case 5:
|
||||
case "MESSAGE_TYPE_COMMUNITY_CHAT":
|
||||
return MessageType.MESSAGE_TYPE_COMMUNITY_CHAT;
|
||||
case 6:
|
||||
case "MESSAGE_TYPE_SYSTEM_MESSAGE_GAP":
|
||||
return MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE_GAP;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return MessageType.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function messageTypeToJSON(object: MessageType): string {
|
||||
switch (object) {
|
||||
case MessageType.MESSAGE_TYPE_UNKNOWN_UNSPECIFIED:
|
||||
return "MESSAGE_TYPE_UNKNOWN_UNSPECIFIED";
|
||||
case MessageType.MESSAGE_TYPE_ONE_TO_ONE:
|
||||
return "MESSAGE_TYPE_ONE_TO_ONE";
|
||||
case MessageType.MESSAGE_TYPE_MESSAGE_TYPE_PUBLIC_GROUP:
|
||||
return "MESSAGE_TYPE_MESSAGE_TYPE_PUBLIC_GROUP";
|
||||
case MessageType.MESSAGE_TYPE_PRIVATE_GROUP:
|
||||
return "MESSAGE_TYPE_PRIVATE_GROUP";
|
||||
case MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP:
|
||||
return "MESSAGE_TYPE_SYSTEM_MESSAGE_PRIVATE_GROUP";
|
||||
case MessageType.MESSAGE_TYPE_COMMUNITY_CHAT:
|
||||
return "MESSAGE_TYPE_COMMUNITY_CHAT";
|
||||
case MessageType.MESSAGE_TYPE_SYSTEM_MESSAGE_GAP:
|
||||
return "MESSAGE_TYPE_SYSTEM_MESSAGE_GAP";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
export enum ImageType {
|
||||
IMAGE_TYPE_UNKNOWN_UNSPECIFIED = 0,
|
||||
/** IMAGE_TYPE_PNG - Raster image files is payload data that can be read as a raster image */
|
||||
IMAGE_TYPE_PNG = 1,
|
||||
IMAGE_TYPE_JPEG = 2,
|
||||
IMAGE_TYPE_WEBP = 3,
|
||||
IMAGE_TYPE_GIF = 4,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function imageTypeFromJSON(object: any): ImageType {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "IMAGE_TYPE_UNKNOWN_UNSPECIFIED":
|
||||
return ImageType.IMAGE_TYPE_UNKNOWN_UNSPECIFIED;
|
||||
case 1:
|
||||
case "IMAGE_TYPE_PNG":
|
||||
return ImageType.IMAGE_TYPE_PNG;
|
||||
case 2:
|
||||
case "IMAGE_TYPE_JPEG":
|
||||
return ImageType.IMAGE_TYPE_JPEG;
|
||||
case 3:
|
||||
case "IMAGE_TYPE_WEBP":
|
||||
return ImageType.IMAGE_TYPE_WEBP;
|
||||
case 4:
|
||||
case "IMAGE_TYPE_GIF":
|
||||
return ImageType.IMAGE_TYPE_GIF;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return ImageType.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function imageTypeToJSON(object: ImageType): string {
|
||||
switch (object) {
|
||||
case ImageType.IMAGE_TYPE_UNKNOWN_UNSPECIFIED:
|
||||
return "IMAGE_TYPE_UNKNOWN_UNSPECIFIED";
|
||||
case ImageType.IMAGE_TYPE_PNG:
|
||||
return "IMAGE_TYPE_PNG";
|
||||
case ImageType.IMAGE_TYPE_JPEG:
|
||||
return "IMAGE_TYPE_JPEG";
|
||||
case ImageType.IMAGE_TYPE_WEBP:
|
||||
return "IMAGE_TYPE_WEBP";
|
||||
case ImageType.IMAGE_TYPE_GIF:
|
||||
return "IMAGE_TYPE_GIF";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
|
@ -0,0 +1,436 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
import { ChatMessage } from "../../communities/v1/chat_message";
|
||||
import { EmojiReaction } from "../../communities/v1/emoji_reaction";
|
||||
|
||||
export const protobufPackage = "communities.v1";
|
||||
|
||||
export interface MembershipUpdateEvent {
|
||||
/** Lamport timestamp of the event */
|
||||
clock: number;
|
||||
/** List of public keys of objects of the action */
|
||||
members: string[];
|
||||
/** Name of the chat for the CHAT_CREATED/NAME_CHANGED event types */
|
||||
name: string;
|
||||
/** The type of the event */
|
||||
type: MembershipUpdateEvent_EventType;
|
||||
}
|
||||
|
||||
export enum MembershipUpdateEvent_EventType {
|
||||
UNKNOWN = 0,
|
||||
CHAT_CREATED = 1,
|
||||
NAME_CHANGED = 2,
|
||||
MEMBERS_ADDED = 3,
|
||||
MEMBER_JOINED = 4,
|
||||
MEMBER_REMOVED = 5,
|
||||
ADMINS_ADDED = 6,
|
||||
ADMIN_REMOVED = 7,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function membershipUpdateEvent_EventTypeFromJSON(
|
||||
object: any
|
||||
): MembershipUpdateEvent_EventType {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "UNKNOWN":
|
||||
return MembershipUpdateEvent_EventType.UNKNOWN;
|
||||
case 1:
|
||||
case "CHAT_CREATED":
|
||||
return MembershipUpdateEvent_EventType.CHAT_CREATED;
|
||||
case 2:
|
||||
case "NAME_CHANGED":
|
||||
return MembershipUpdateEvent_EventType.NAME_CHANGED;
|
||||
case 3:
|
||||
case "MEMBERS_ADDED":
|
||||
return MembershipUpdateEvent_EventType.MEMBERS_ADDED;
|
||||
case 4:
|
||||
case "MEMBER_JOINED":
|
||||
return MembershipUpdateEvent_EventType.MEMBER_JOINED;
|
||||
case 5:
|
||||
case "MEMBER_REMOVED":
|
||||
return MembershipUpdateEvent_EventType.MEMBER_REMOVED;
|
||||
case 6:
|
||||
case "ADMINS_ADDED":
|
||||
return MembershipUpdateEvent_EventType.ADMINS_ADDED;
|
||||
case 7:
|
||||
case "ADMIN_REMOVED":
|
||||
return MembershipUpdateEvent_EventType.ADMIN_REMOVED;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return MembershipUpdateEvent_EventType.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function membershipUpdateEvent_EventTypeToJSON(
|
||||
object: MembershipUpdateEvent_EventType
|
||||
): string {
|
||||
switch (object) {
|
||||
case MembershipUpdateEvent_EventType.UNKNOWN:
|
||||
return "UNKNOWN";
|
||||
case MembershipUpdateEvent_EventType.CHAT_CREATED:
|
||||
return "CHAT_CREATED";
|
||||
case MembershipUpdateEvent_EventType.NAME_CHANGED:
|
||||
return "NAME_CHANGED";
|
||||
case MembershipUpdateEvent_EventType.MEMBERS_ADDED:
|
||||
return "MEMBERS_ADDED";
|
||||
case MembershipUpdateEvent_EventType.MEMBER_JOINED:
|
||||
return "MEMBER_JOINED";
|
||||
case MembershipUpdateEvent_EventType.MEMBER_REMOVED:
|
||||
return "MEMBER_REMOVED";
|
||||
case MembershipUpdateEvent_EventType.ADMINS_ADDED:
|
||||
return "ADMINS_ADDED";
|
||||
case MembershipUpdateEvent_EventType.ADMIN_REMOVED:
|
||||
return "ADMIN_REMOVED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MembershipUpdateMessage is a message used to propagate information
|
||||
* about group membership changes.
|
||||
* For more information, see https://github.com/status-im/specs/blob/master/status-group-chats-spec.md.
|
||||
*/
|
||||
export interface MembershipUpdateMessage {
|
||||
/** The chat id of the private group chat */
|
||||
chatId: string;
|
||||
/**
|
||||
* A list of events for this group chat, first x bytes are the signature, then is a
|
||||
* protobuf encoded MembershipUpdateEvent
|
||||
*/
|
||||
events: Uint8Array[];
|
||||
message: ChatMessage | undefined;
|
||||
emojiReaction: EmojiReaction | undefined;
|
||||
}
|
||||
|
||||
const baseMembershipUpdateEvent: object = {
|
||||
clock: 0,
|
||||
members: "",
|
||||
name: "",
|
||||
type: 0,
|
||||
};
|
||||
|
||||
export const MembershipUpdateEvent = {
|
||||
encode(
|
||||
message: MembershipUpdateEvent,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.clock !== 0) {
|
||||
writer.uint32(8).uint64(message.clock);
|
||||
}
|
||||
for (const v of message.members) {
|
||||
writer.uint32(18).string(v!);
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(26).string(message.name);
|
||||
}
|
||||
if (message.type !== 0) {
|
||||
writer.uint32(32).int32(message.type);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(
|
||||
input: _m0.Reader | Uint8Array,
|
||||
length?: number
|
||||
): MembershipUpdateEvent {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = { ...baseMembershipUpdateEvent } as MembershipUpdateEvent;
|
||||
message.members = [];
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.clock = longToNumber(reader.uint64() as Long);
|
||||
break;
|
||||
case 2:
|
||||
message.members.push(reader.string());
|
||||
break;
|
||||
case 3:
|
||||
message.name = reader.string();
|
||||
break;
|
||||
case 4:
|
||||
message.type = reader.int32() as any;
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): MembershipUpdateEvent {
|
||||
const message = { ...baseMembershipUpdateEvent } as MembershipUpdateEvent;
|
||||
message.members = [];
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = Number(object.clock);
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.members !== undefined && object.members !== null) {
|
||||
for (const e of object.members) {
|
||||
message.members.push(String(e));
|
||||
}
|
||||
}
|
||||
if (object.name !== undefined && object.name !== null) {
|
||||
message.name = String(object.name);
|
||||
} else {
|
||||
message.name = "";
|
||||
}
|
||||
if (object.type !== undefined && object.type !== null) {
|
||||
message.type = membershipUpdateEvent_EventTypeFromJSON(object.type);
|
||||
} else {
|
||||
message.type = 0;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: MembershipUpdateEvent): unknown {
|
||||
const obj: any = {};
|
||||
message.clock !== undefined && (obj.clock = message.clock);
|
||||
if (message.members) {
|
||||
obj.members = message.members.map((e) => e);
|
||||
} else {
|
||||
obj.members = [];
|
||||
}
|
||||
message.name !== undefined && (obj.name = message.name);
|
||||
message.type !== undefined &&
|
||||
(obj.type = membershipUpdateEvent_EventTypeToJSON(message.type));
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(
|
||||
object: DeepPartial<MembershipUpdateEvent>
|
||||
): MembershipUpdateEvent {
|
||||
const message = { ...baseMembershipUpdateEvent } as MembershipUpdateEvent;
|
||||
message.members = [];
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = object.clock;
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.members !== undefined && object.members !== null) {
|
||||
for (const e of object.members) {
|
||||
message.members.push(e);
|
||||
}
|
||||
}
|
||||
if (object.name !== undefined && object.name !== null) {
|
||||
message.name = object.name;
|
||||
} else {
|
||||
message.name = "";
|
||||
}
|
||||
if (object.type !== undefined && object.type !== null) {
|
||||
message.type = object.type;
|
||||
} else {
|
||||
message.type = 0;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
const baseMembershipUpdateMessage: object = { chatId: "" };
|
||||
|
||||
export const MembershipUpdateMessage = {
|
||||
encode(
|
||||
message: MembershipUpdateMessage,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.chatId !== "") {
|
||||
writer.uint32(10).string(message.chatId);
|
||||
}
|
||||
for (const v of message.events) {
|
||||
writer.uint32(18).bytes(v!);
|
||||
}
|
||||
if (message.message !== undefined) {
|
||||
ChatMessage.encode(message.message, writer.uint32(26).fork()).ldelim();
|
||||
}
|
||||
if (message.emojiReaction !== undefined) {
|
||||
EmojiReaction.encode(
|
||||
message.emojiReaction,
|
||||
writer.uint32(34).fork()
|
||||
).ldelim();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(
|
||||
input: _m0.Reader | Uint8Array,
|
||||
length?: number
|
||||
): MembershipUpdateMessage {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = {
|
||||
...baseMembershipUpdateMessage,
|
||||
} as MembershipUpdateMessage;
|
||||
message.events = [];
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.chatId = reader.string();
|
||||
break;
|
||||
case 2:
|
||||
message.events.push(reader.bytes());
|
||||
break;
|
||||
case 3:
|
||||
message.message = ChatMessage.decode(reader, reader.uint32());
|
||||
break;
|
||||
case 4:
|
||||
message.emojiReaction = EmojiReaction.decode(reader, reader.uint32());
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): MembershipUpdateMessage {
|
||||
const message = {
|
||||
...baseMembershipUpdateMessage,
|
||||
} as MembershipUpdateMessage;
|
||||
message.events = [];
|
||||
if (object.chatId !== undefined && object.chatId !== null) {
|
||||
message.chatId = String(object.chatId);
|
||||
} else {
|
||||
message.chatId = "";
|
||||
}
|
||||
if (object.events !== undefined && object.events !== null) {
|
||||
for (const e of object.events) {
|
||||
message.events.push(bytesFromBase64(e));
|
||||
}
|
||||
}
|
||||
if (object.message !== undefined && object.message !== null) {
|
||||
message.message = ChatMessage.fromJSON(object.message);
|
||||
} else {
|
||||
message.message = undefined;
|
||||
}
|
||||
if (object.emojiReaction !== undefined && object.emojiReaction !== null) {
|
||||
message.emojiReaction = EmojiReaction.fromJSON(object.emojiReaction);
|
||||
} else {
|
||||
message.emojiReaction = undefined;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: MembershipUpdateMessage): unknown {
|
||||
const obj: any = {};
|
||||
message.chatId !== undefined && (obj.chatId = message.chatId);
|
||||
if (message.events) {
|
||||
obj.events = message.events.map((e) =>
|
||||
base64FromBytes(e !== undefined ? e : new Uint8Array())
|
||||
);
|
||||
} else {
|
||||
obj.events = [];
|
||||
}
|
||||
message.message !== undefined &&
|
||||
(obj.message = message.message
|
||||
? ChatMessage.toJSON(message.message)
|
||||
: undefined);
|
||||
message.emojiReaction !== undefined &&
|
||||
(obj.emojiReaction = message.emojiReaction
|
||||
? EmojiReaction.toJSON(message.emojiReaction)
|
||||
: undefined);
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(
|
||||
object: DeepPartial<MembershipUpdateMessage>
|
||||
): MembershipUpdateMessage {
|
||||
const message = {
|
||||
...baseMembershipUpdateMessage,
|
||||
} as MembershipUpdateMessage;
|
||||
message.events = [];
|
||||
if (object.chatId !== undefined && object.chatId !== null) {
|
||||
message.chatId = object.chatId;
|
||||
} else {
|
||||
message.chatId = "";
|
||||
}
|
||||
if (object.events !== undefined && object.events !== null) {
|
||||
for (const e of object.events) {
|
||||
message.events.push(e);
|
||||
}
|
||||
}
|
||||
if (object.message !== undefined && object.message !== null) {
|
||||
message.message = ChatMessage.fromPartial(object.message);
|
||||
} else {
|
||||
message.message = undefined;
|
||||
}
|
||||
if (object.emojiReaction !== undefined && object.emojiReaction !== null) {
|
||||
message.emojiReaction = EmojiReaction.fromPartial(object.emojiReaction);
|
||||
} else {
|
||||
message.emojiReaction = undefined;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
declare var self: any | undefined;
|
||||
declare var window: any | undefined;
|
||||
declare var global: any | undefined;
|
||||
var globalThis: any = (() => {
|
||||
if (typeof globalThis !== "undefined") return globalThis;
|
||||
if (typeof self !== "undefined") return self;
|
||||
if (typeof window !== "undefined") return window;
|
||||
if (typeof global !== "undefined") return global;
|
||||
throw "Unable to locate global object";
|
||||
})();
|
||||
|
||||
const atob: (b64: string) => string =
|
||||
globalThis.atob ||
|
||||
((b64) => globalThis.Buffer.from(b64, "base64").toString("binary"));
|
||||
function bytesFromBase64(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; ++i) {
|
||||
arr[i] = bin.charCodeAt(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const btoa: (bin: string) => string =
|
||||
globalThis.btoa ||
|
||||
((bin) => globalThis.Buffer.from(bin, "binary").toString("base64"));
|
||||
function base64FromBytes(arr: Uint8Array): string {
|
||||
const bin: string[] = [];
|
||||
for (const byte of arr) {
|
||||
bin.push(String.fromCharCode(byte));
|
||||
}
|
||||
return btoa(bin.join(""));
|
||||
}
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
function longToNumber(long: Long): number {
|
||||
if (long.gt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
return long.toNumber();
|
||||
}
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "communities.v1";
|
||||
|
||||
/**
|
||||
* Specs:
|
||||
* :AUTOMATIC
|
||||
* To Send - "AUTOMATIC" status ping every 5 minutes
|
||||
* Display - Online for up to 5 minutes from the last clock, after that Offline
|
||||
* :ALWAYS_ONLINE
|
||||
* To Send - "ALWAYS_ONLINE" status ping every 5 minutes
|
||||
* Display - Online for up to 2 weeks from the last clock, after that Offline
|
||||
* :INACTIVE
|
||||
* To Send - A single "INACTIVE" status ping
|
||||
* Display - Offline forever
|
||||
* Note: Only send pings if the user interacted with the app in the last x minutes.
|
||||
*/
|
||||
export interface StatusUpdate {
|
||||
clock: number;
|
||||
statusType: StatusUpdate_StatusType;
|
||||
customText: string;
|
||||
}
|
||||
|
||||
export enum StatusUpdate_StatusType {
|
||||
UNKNOWN_STATUS_TYPE = 0,
|
||||
AUTOMATIC = 1,
|
||||
DO_NOT_DISTURB = 2,
|
||||
ALWAYS_ONLINE = 3,
|
||||
INACTIVE = 4,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function statusUpdate_StatusTypeFromJSON(
|
||||
object: any
|
||||
): StatusUpdate_StatusType {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "UNKNOWN_STATUS_TYPE":
|
||||
return StatusUpdate_StatusType.UNKNOWN_STATUS_TYPE;
|
||||
case 1:
|
||||
case "AUTOMATIC":
|
||||
return StatusUpdate_StatusType.AUTOMATIC;
|
||||
case 2:
|
||||
case "DO_NOT_DISTURB":
|
||||
return StatusUpdate_StatusType.DO_NOT_DISTURB;
|
||||
case 3:
|
||||
case "ALWAYS_ONLINE":
|
||||
return StatusUpdate_StatusType.ALWAYS_ONLINE;
|
||||
case 4:
|
||||
case "INACTIVE":
|
||||
return StatusUpdate_StatusType.INACTIVE;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return StatusUpdate_StatusType.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function statusUpdate_StatusTypeToJSON(
|
||||
object: StatusUpdate_StatusType
|
||||
): string {
|
||||
switch (object) {
|
||||
case StatusUpdate_StatusType.UNKNOWN_STATUS_TYPE:
|
||||
return "UNKNOWN_STATUS_TYPE";
|
||||
case StatusUpdate_StatusType.AUTOMATIC:
|
||||
return "AUTOMATIC";
|
||||
case StatusUpdate_StatusType.DO_NOT_DISTURB:
|
||||
return "DO_NOT_DISTURB";
|
||||
case StatusUpdate_StatusType.ALWAYS_ONLINE:
|
||||
return "ALWAYS_ONLINE";
|
||||
case StatusUpdate_StatusType.INACTIVE:
|
||||
return "INACTIVE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const baseStatusUpdate: object = { clock: 0, statusType: 0, customText: "" };
|
||||
|
||||
export const StatusUpdate = {
|
||||
encode(
|
||||
message: StatusUpdate,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.clock !== 0) {
|
||||
writer.uint32(8).uint64(message.clock);
|
||||
}
|
||||
if (message.statusType !== 0) {
|
||||
writer.uint32(16).int32(message.statusType);
|
||||
}
|
||||
if (message.customText !== "") {
|
||||
writer.uint32(26).string(message.customText);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): StatusUpdate {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = { ...baseStatusUpdate } as StatusUpdate;
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.clock = longToNumber(reader.uint64() as Long);
|
||||
break;
|
||||
case 2:
|
||||
message.statusType = reader.int32() as any;
|
||||
break;
|
||||
case 3:
|
||||
message.customText = reader.string();
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): StatusUpdate {
|
||||
const message = { ...baseStatusUpdate } as StatusUpdate;
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = Number(object.clock);
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.statusType !== undefined && object.statusType !== null) {
|
||||
message.statusType = statusUpdate_StatusTypeFromJSON(object.statusType);
|
||||
} else {
|
||||
message.statusType = 0;
|
||||
}
|
||||
if (object.customText !== undefined && object.customText !== null) {
|
||||
message.customText = String(object.customText);
|
||||
} else {
|
||||
message.customText = "";
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: StatusUpdate): unknown {
|
||||
const obj: any = {};
|
||||
message.clock !== undefined && (obj.clock = message.clock);
|
||||
message.statusType !== undefined &&
|
||||
(obj.statusType = statusUpdate_StatusTypeToJSON(message.statusType));
|
||||
message.customText !== undefined && (obj.customText = message.customText);
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(object: DeepPartial<StatusUpdate>): StatusUpdate {
|
||||
const message = { ...baseStatusUpdate } as StatusUpdate;
|
||||
if (object.clock !== undefined && object.clock !== null) {
|
||||
message.clock = object.clock;
|
||||
} else {
|
||||
message.clock = 0;
|
||||
}
|
||||
if (object.statusType !== undefined && object.statusType !== null) {
|
||||
message.statusType = object.statusType;
|
||||
} else {
|
||||
message.statusType = 0;
|
||||
}
|
||||
if (object.customText !== undefined && object.customText !== null) {
|
||||
message.customText = object.customText;
|
||||
} else {
|
||||
message.customText = "";
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
declare var self: any | undefined;
|
||||
declare var window: any | undefined;
|
||||
declare var global: any | undefined;
|
||||
var globalThis: any = (() => {
|
||||
if (typeof globalThis !== "undefined") return globalThis;
|
||||
if (typeof self !== "undefined") return self;
|
||||
if (typeof window !== "undefined") return window;
|
||||
if (typeof global !== "undefined") return global;
|
||||
throw "Unable to locate global object";
|
||||
})();
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
function longToNumber(long: Long): number {
|
||||
if (long.gt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
return long.toNumber();
|
||||
}
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "status.v1";
|
||||
|
||||
export interface ApplicationMetadataMessage {
|
||||
/** Signature of the payload field */
|
||||
signature: Uint8Array;
|
||||
/** This is the encoded protobuf of the application level message, i.e ChatMessage */
|
||||
payload: Uint8Array;
|
||||
/** The type of protobuf message sent */
|
||||
type: ApplicationMetadataMessage_Type;
|
||||
}
|
||||
|
||||
export enum ApplicationMetadataMessage_Type {
|
||||
TYPE_UNKNOWN_UNSPECIFIED = 0,
|
||||
TYPE_CHAT_MESSAGE = 1,
|
||||
TYPE_CONTACT_UPDATE = 2,
|
||||
TYPE_MEMBERSHIP_UPDATE_MESSAGE = 3,
|
||||
TYPE_PAIR_INSTALLATION = 4,
|
||||
TYPE_SYNC_INSTALLATION = 5,
|
||||
TYPE_REQUEST_ADDRESS_FOR_TRANSACTION = 6,
|
||||
TYPE_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION = 7,
|
||||
TYPE_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION = 8,
|
||||
TYPE_REQUEST_TRANSACTION = 9,
|
||||
TYPE_SEND_TRANSACTION = 10,
|
||||
TYPE_DECLINE_REQUEST_TRANSACTION = 11,
|
||||
TYPE_SYNC_INSTALLATION_CONTACT = 12,
|
||||
TYPE_SYNC_INSTALLATION_ACCOUNT = 13,
|
||||
TYPE_SYNC_INSTALLATION_PUBLIC_CHAT = 14,
|
||||
TYPE_CONTACT_CODE_ADVERTISEMENT = 15,
|
||||
TYPE_PUSH_NOTIFICATION_REGISTRATION = 16,
|
||||
TYPE_PUSH_NOTIFICATION_REGISTRATION_RESPONSE = 17,
|
||||
TYPE_PUSH_NOTIFICATION_QUERY = 18,
|
||||
TYPE_PUSH_NOTIFICATION_QUERY_RESPONSE = 19,
|
||||
TYPE_PUSH_NOTIFICATION_REQUEST = 20,
|
||||
TYPE_PUSH_NOTIFICATION_RESPONSE = 21,
|
||||
TYPE_EMOJI_REACTION = 22,
|
||||
TYPE_GROUP_CHAT_INVITATION = 23,
|
||||
TYPE_CHAT_IDENTITY = 24,
|
||||
TYPE_COMMUNITY_DESCRIPTION = 25,
|
||||
TYPE_COMMUNITY_INVITATION = 26,
|
||||
TYPE_COMMUNITY_REQUEST_TO_JOIN = 27,
|
||||
TYPE_PIN_MESSAGE = 28,
|
||||
TYPE_EDIT_MESSAGE = 29,
|
||||
TYPE_STATUS_UPDATE = 30,
|
||||
TYPE_DELETE_MESSAGE = 31,
|
||||
TYPE_SYNC_INSTALLATION_COMMUNITY = 32,
|
||||
TYPE_ANONYMOUS_METRIC_BATCH = 33,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
export function applicationMetadataMessage_TypeFromJSON(
|
||||
object: any
|
||||
): ApplicationMetadataMessage_Type {
|
||||
switch (object) {
|
||||
case 0:
|
||||
case "TYPE_UNKNOWN_UNSPECIFIED":
|
||||
return ApplicationMetadataMessage_Type.TYPE_UNKNOWN_UNSPECIFIED;
|
||||
case 1:
|
||||
case "TYPE_CHAT_MESSAGE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE;
|
||||
case 2:
|
||||
case "TYPE_CONTACT_UPDATE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_CONTACT_UPDATE;
|
||||
case 3:
|
||||
case "TYPE_MEMBERSHIP_UPDATE_MESSAGE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_MEMBERSHIP_UPDATE_MESSAGE;
|
||||
case 4:
|
||||
case "TYPE_PAIR_INSTALLATION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PAIR_INSTALLATION;
|
||||
case 5:
|
||||
case "TYPE_SYNC_INSTALLATION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION;
|
||||
case 6:
|
||||
case "TYPE_REQUEST_ADDRESS_FOR_TRANSACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_REQUEST_ADDRESS_FOR_TRANSACTION;
|
||||
case 7:
|
||||
case "TYPE_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION;
|
||||
case 8:
|
||||
case "TYPE_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION;
|
||||
case 9:
|
||||
case "TYPE_REQUEST_TRANSACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_REQUEST_TRANSACTION;
|
||||
case 10:
|
||||
case "TYPE_SEND_TRANSACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_SEND_TRANSACTION;
|
||||
case 11:
|
||||
case "TYPE_DECLINE_REQUEST_TRANSACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_DECLINE_REQUEST_TRANSACTION;
|
||||
case 12:
|
||||
case "TYPE_SYNC_INSTALLATION_CONTACT":
|
||||
return ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_CONTACT;
|
||||
case 13:
|
||||
case "TYPE_SYNC_INSTALLATION_ACCOUNT":
|
||||
return ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_ACCOUNT;
|
||||
case 14:
|
||||
case "TYPE_SYNC_INSTALLATION_PUBLIC_CHAT":
|
||||
return ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_PUBLIC_CHAT;
|
||||
case 15:
|
||||
case "TYPE_CONTACT_CODE_ADVERTISEMENT":
|
||||
return ApplicationMetadataMessage_Type.TYPE_CONTACT_CODE_ADVERTISEMENT;
|
||||
case 16:
|
||||
case "TYPE_PUSH_NOTIFICATION_REGISTRATION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_REGISTRATION;
|
||||
case 17:
|
||||
case "TYPE_PUSH_NOTIFICATION_REGISTRATION_RESPONSE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_REGISTRATION_RESPONSE;
|
||||
case 18:
|
||||
case "TYPE_PUSH_NOTIFICATION_QUERY":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_QUERY;
|
||||
case 19:
|
||||
case "TYPE_PUSH_NOTIFICATION_QUERY_RESPONSE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_QUERY_RESPONSE;
|
||||
case 20:
|
||||
case "TYPE_PUSH_NOTIFICATION_REQUEST":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_REQUEST;
|
||||
case 21:
|
||||
case "TYPE_PUSH_NOTIFICATION_RESPONSE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_RESPONSE;
|
||||
case 22:
|
||||
case "TYPE_EMOJI_REACTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_EMOJI_REACTION;
|
||||
case 23:
|
||||
case "TYPE_GROUP_CHAT_INVITATION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_GROUP_CHAT_INVITATION;
|
||||
case 24:
|
||||
case "TYPE_CHAT_IDENTITY":
|
||||
return ApplicationMetadataMessage_Type.TYPE_CHAT_IDENTITY;
|
||||
case 25:
|
||||
case "TYPE_COMMUNITY_DESCRIPTION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_COMMUNITY_DESCRIPTION;
|
||||
case 26:
|
||||
case "TYPE_COMMUNITY_INVITATION":
|
||||
return ApplicationMetadataMessage_Type.TYPE_COMMUNITY_INVITATION;
|
||||
case 27:
|
||||
case "TYPE_COMMUNITY_REQUEST_TO_JOIN":
|
||||
return ApplicationMetadataMessage_Type.TYPE_COMMUNITY_REQUEST_TO_JOIN;
|
||||
case 28:
|
||||
case "TYPE_PIN_MESSAGE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_PIN_MESSAGE;
|
||||
case 29:
|
||||
case "TYPE_EDIT_MESSAGE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_EDIT_MESSAGE;
|
||||
case 30:
|
||||
case "TYPE_STATUS_UPDATE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_STATUS_UPDATE;
|
||||
case 31:
|
||||
case "TYPE_DELETE_MESSAGE":
|
||||
return ApplicationMetadataMessage_Type.TYPE_DELETE_MESSAGE;
|
||||
case 32:
|
||||
case "TYPE_SYNC_INSTALLATION_COMMUNITY":
|
||||
return ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_COMMUNITY;
|
||||
case 33:
|
||||
case "TYPE_ANONYMOUS_METRIC_BATCH":
|
||||
return ApplicationMetadataMessage_Type.TYPE_ANONYMOUS_METRIC_BATCH;
|
||||
case -1:
|
||||
case "UNRECOGNIZED":
|
||||
default:
|
||||
return ApplicationMetadataMessage_Type.UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export function applicationMetadataMessage_TypeToJSON(
|
||||
object: ApplicationMetadataMessage_Type
|
||||
): string {
|
||||
switch (object) {
|
||||
case ApplicationMetadataMessage_Type.TYPE_UNKNOWN_UNSPECIFIED:
|
||||
return "TYPE_UNKNOWN_UNSPECIFIED";
|
||||
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
|
||||
return "TYPE_CHAT_MESSAGE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_CONTACT_UPDATE:
|
||||
return "TYPE_CONTACT_UPDATE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_MEMBERSHIP_UPDATE_MESSAGE:
|
||||
return "TYPE_MEMBERSHIP_UPDATE_MESSAGE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PAIR_INSTALLATION:
|
||||
return "TYPE_PAIR_INSTALLATION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION:
|
||||
return "TYPE_SYNC_INSTALLATION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_REQUEST_ADDRESS_FOR_TRANSACTION:
|
||||
return "TYPE_REQUEST_ADDRESS_FOR_TRANSACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION:
|
||||
return "TYPE_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION:
|
||||
return "TYPE_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_REQUEST_TRANSACTION:
|
||||
return "TYPE_REQUEST_TRANSACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_SEND_TRANSACTION:
|
||||
return "TYPE_SEND_TRANSACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_DECLINE_REQUEST_TRANSACTION:
|
||||
return "TYPE_DECLINE_REQUEST_TRANSACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_CONTACT:
|
||||
return "TYPE_SYNC_INSTALLATION_CONTACT";
|
||||
case ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_ACCOUNT:
|
||||
return "TYPE_SYNC_INSTALLATION_ACCOUNT";
|
||||
case ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_PUBLIC_CHAT:
|
||||
return "TYPE_SYNC_INSTALLATION_PUBLIC_CHAT";
|
||||
case ApplicationMetadataMessage_Type.TYPE_CONTACT_CODE_ADVERTISEMENT:
|
||||
return "TYPE_CONTACT_CODE_ADVERTISEMENT";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_REGISTRATION:
|
||||
return "TYPE_PUSH_NOTIFICATION_REGISTRATION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_REGISTRATION_RESPONSE:
|
||||
return "TYPE_PUSH_NOTIFICATION_REGISTRATION_RESPONSE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_QUERY:
|
||||
return "TYPE_PUSH_NOTIFICATION_QUERY";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_QUERY_RESPONSE:
|
||||
return "TYPE_PUSH_NOTIFICATION_QUERY_RESPONSE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_REQUEST:
|
||||
return "TYPE_PUSH_NOTIFICATION_REQUEST";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PUSH_NOTIFICATION_RESPONSE:
|
||||
return "TYPE_PUSH_NOTIFICATION_RESPONSE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_EMOJI_REACTION:
|
||||
return "TYPE_EMOJI_REACTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_GROUP_CHAT_INVITATION:
|
||||
return "TYPE_GROUP_CHAT_INVITATION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_CHAT_IDENTITY:
|
||||
return "TYPE_CHAT_IDENTITY";
|
||||
case ApplicationMetadataMessage_Type.TYPE_COMMUNITY_DESCRIPTION:
|
||||
return "TYPE_COMMUNITY_DESCRIPTION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_COMMUNITY_INVITATION:
|
||||
return "TYPE_COMMUNITY_INVITATION";
|
||||
case ApplicationMetadataMessage_Type.TYPE_COMMUNITY_REQUEST_TO_JOIN:
|
||||
return "TYPE_COMMUNITY_REQUEST_TO_JOIN";
|
||||
case ApplicationMetadataMessage_Type.TYPE_PIN_MESSAGE:
|
||||
return "TYPE_PIN_MESSAGE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_EDIT_MESSAGE:
|
||||
return "TYPE_EDIT_MESSAGE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_STATUS_UPDATE:
|
||||
return "TYPE_STATUS_UPDATE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_DELETE_MESSAGE:
|
||||
return "TYPE_DELETE_MESSAGE";
|
||||
case ApplicationMetadataMessage_Type.TYPE_SYNC_INSTALLATION_COMMUNITY:
|
||||
return "TYPE_SYNC_INSTALLATION_COMMUNITY";
|
||||
case ApplicationMetadataMessage_Type.TYPE_ANONYMOUS_METRIC_BATCH:
|
||||
return "TYPE_ANONYMOUS_METRIC_BATCH";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const baseApplicationMetadataMessage: object = { type: 0 };
|
||||
|
||||
export const ApplicationMetadataMessage = {
|
||||
encode(
|
||||
message: ApplicationMetadataMessage,
|
||||
writer: _m0.Writer = _m0.Writer.create()
|
||||
): _m0.Writer {
|
||||
if (message.signature.length !== 0) {
|
||||
writer.uint32(10).bytes(message.signature);
|
||||
}
|
||||
if (message.payload.length !== 0) {
|
||||
writer.uint32(18).bytes(message.payload);
|
||||
}
|
||||
if (message.type !== 0) {
|
||||
writer.uint32(24).int32(message.type);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(
|
||||
input: _m0.Reader | Uint8Array,
|
||||
length?: number
|
||||
): ApplicationMetadataMessage {
|
||||
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = {
|
||||
...baseApplicationMetadataMessage,
|
||||
} as ApplicationMetadataMessage;
|
||||
message.signature = new Uint8Array();
|
||||
message.payload = new Uint8Array();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
message.signature = reader.bytes();
|
||||
break;
|
||||
case 2:
|
||||
message.payload = reader.bytes();
|
||||
break;
|
||||
case 3:
|
||||
message.type = reader.int32() as any;
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ApplicationMetadataMessage {
|
||||
const message = {
|
||||
...baseApplicationMetadataMessage,
|
||||
} as ApplicationMetadataMessage;
|
||||
message.signature = new Uint8Array();
|
||||
message.payload = new Uint8Array();
|
||||
if (object.signature !== undefined && object.signature !== null) {
|
||||
message.signature = bytesFromBase64(object.signature);
|
||||
}
|
||||
if (object.payload !== undefined && object.payload !== null) {
|
||||
message.payload = bytesFromBase64(object.payload);
|
||||
}
|
||||
if (object.type !== undefined && object.type !== null) {
|
||||
message.type = applicationMetadataMessage_TypeFromJSON(object.type);
|
||||
} else {
|
||||
message.type = 0;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
toJSON(message: ApplicationMetadataMessage): unknown {
|
||||
const obj: any = {};
|
||||
message.signature !== undefined &&
|
||||
(obj.signature = base64FromBytes(
|
||||
message.signature !== undefined ? message.signature : new Uint8Array()
|
||||
));
|
||||
message.payload !== undefined &&
|
||||
(obj.payload = base64FromBytes(
|
||||
message.payload !== undefined ? message.payload : new Uint8Array()
|
||||
));
|
||||
message.type !== undefined &&
|
||||
(obj.type = applicationMetadataMessage_TypeToJSON(message.type));
|
||||
return obj;
|
||||
},
|
||||
|
||||
fromPartial(
|
||||
object: DeepPartial<ApplicationMetadataMessage>
|
||||
): ApplicationMetadataMessage {
|
||||
const message = {
|
||||
...baseApplicationMetadataMessage,
|
||||
} as ApplicationMetadataMessage;
|
||||
if (object.signature !== undefined && object.signature !== null) {
|
||||
message.signature = object.signature;
|
||||
} else {
|
||||
message.signature = new Uint8Array();
|
||||
}
|
||||
if (object.payload !== undefined && object.payload !== null) {
|
||||
message.payload = object.payload;
|
||||
} else {
|
||||
message.payload = new Uint8Array();
|
||||
}
|
||||
if (object.type !== undefined && object.type !== null) {
|
||||
message.type = object.type;
|
||||
} else {
|
||||
message.type = 0;
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
declare var self: any | undefined;
|
||||
declare var window: any | undefined;
|
||||
declare var global: any | undefined;
|
||||
var globalThis: any = (() => {
|
||||
if (typeof globalThis !== "undefined") return globalThis;
|
||||
if (typeof self !== "undefined") return self;
|
||||
if (typeof window !== "undefined") return window;
|
||||
if (typeof global !== "undefined") return global;
|
||||
throw "Unable to locate global object";
|
||||
})();
|
||||
|
||||
const atob: (b64: string) => string =
|
||||
globalThis.atob ||
|
||||
((b64) => globalThis.Buffer.from(b64, "base64").toString("binary"));
|
||||
function bytesFromBase64(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; ++i) {
|
||||
arr[i] = bin.charCodeAt(i);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const btoa: (bin: string) => string =
|
||||
globalThis.btoa ||
|
||||
((bin) => globalThis.Buffer.from(bin, "binary").toString("base64"));
|
||||
function base64FromBytes(arr: Uint8Array): string {
|
||||
const bin: string[] = [];
|
||||
for (const byte of arr) {
|
||||
bin.push(String.fromCharCode(byte));
|
||||
}
|
||||
return btoa(bin.join(""));
|
||||
}
|
||||
|
||||
type Builtin =
|
||||
| Date
|
||||
| Function
|
||||
| Uint8Array
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined;
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {}
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { BN } from "bn.js";
|
||||
import { derive } from "ecies-geth";
|
||||
import { ec } from "elliptic";
|
||||
import { bufToHex } from "js-waku/build/main/lib/utils";
|
||||
|
||||
import { idToContentTopic } from "./contentTopic";
|
||||
import { hexToBuf } from "./utils";
|
||||
|
||||
import { Identity } from ".";
|
||||
|
||||
const EC = new ec("secp256k1");
|
||||
const partitionsNum = new BN(5000);
|
||||
|
||||
/**
|
||||
* Get the partitioned topic https://specs.status.im/spec/3#partitioned-topic
|
||||
* @param publicKey Public key of recipient
|
||||
* @returns string The Waku v2 Content Topic.
|
||||
*/
|
||||
export function getPartitionedTopic(publicKey: string): string {
|
||||
const key = EC.keyFromPublic(publicKey.slice(2), "hex");
|
||||
const X = key.getPublic().getX();
|
||||
|
||||
const partition = X.mod(partitionsNum);
|
||||
|
||||
const partitionTopic = `contact-discovery-${partition.toString()}`;
|
||||
|
||||
return idToContentTopic(partitionTopic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the negotiated topic https://specs.status.im/spec/3#negotiated-topic
|
||||
* @param identity identity of user
|
||||
* @param publicKey Public key of recipient
|
||||
* @returns string The Waku v2 Content Topic.
|
||||
*/
|
||||
export async function getNegotiatedTopic(
|
||||
identity: Identity,
|
||||
publicKey: string
|
||||
): Promise<string> {
|
||||
const key = EC.keyFromPublic(publicKey.slice(2), "hex");
|
||||
const sharedSecret = await derive(
|
||||
Buffer.from(identity.privateKey),
|
||||
hexToBuf(key.getPublic("hex"))
|
||||
);
|
||||
return idToContentTopic(bufToHex(sharedSecret));
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { ec } from "elliptic";
|
||||
import { PageDirection, utils, Waku } from "js-waku";
|
||||
|
||||
import { idToContactCodeTopic } from "./contentTopic";
|
||||
import { ChatIdentity } from "./proto/communities/v1/chat_identity";
|
||||
|
||||
const EC = new ec("secp256k1");
|
||||
|
||||
const hexToBuf = utils.hexToBuf;
|
||||
export { hexToBuf };
|
||||
|
||||
/**
|
||||
* Return hex string with 0x prefix (commonly used for string format of a community id/public key.
|
||||
*/
|
||||
export function bufToHex(buf: Uint8Array): string {
|
||||
return "0x" + utils.bufToHex(buf);
|
||||
}
|
||||
|
||||
export function compressPublicKey(key: Uint8Array): string {
|
||||
const PubKey = EC.keyFromPublic(key);
|
||||
return "0x" + PubKey.getPublic(true, "hex");
|
||||
}
|
||||
|
||||
export function genPrivateKeyWithEntropy(key: string): Uint8Array {
|
||||
const pair = EC.genKeyPair({ entropy: key });
|
||||
return hexToBuf("0x" + pair.getPrivate("hex"));
|
||||
}
|
||||
|
||||
export async function getLatestUserNickname(
|
||||
key: Uint8Array,
|
||||
waku: Waku
|
||||
): Promise<{ clock: number; nickname: string }> {
|
||||
const publicKey = bufToHex(key);
|
||||
let nickname = "";
|
||||
let clock = 0;
|
||||
await waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
|
||||
callback: (msgs) =>
|
||||
msgs.some((e) => {
|
||||
try {
|
||||
if (e.payload) {
|
||||
const chatIdentity = ChatIdentity.decode(e?.payload);
|
||||
if (chatIdentity) {
|
||||
if (chatIdentity?.displayName) {
|
||||
clock = chatIdentity?.clock ?? 0;
|
||||
nickname = chatIdentity?.displayName;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
pageDirection: PageDirection.BACKWARD,
|
||||
});
|
||||
return { clock, nickname };
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import { keccak256 } from "js-sha3";
|
||||
import { Reader } from "protobufjs";
|
||||
import secp256k1 from "secp256k1";
|
||||
|
||||
import { Identity } from "../identity";
|
||||
import * as proto from "../proto/status/v1/application_metadata_message";
|
||||
import { ApplicationMetadataMessage_Type } from "../proto/status/v1/application_metadata_message";
|
||||
import { hexToBuf } from "../utils";
|
||||
|
||||
import { ChatMessage } from "./chat_message";
|
||||
|
||||
export class ApplicationMetadataMessage {
|
||||
private constructor(public proto: proto.ApplicationMetadataMessage) {}
|
||||
|
||||
/**
|
||||
* Create a chat message to be sent to an Open (permission = no membership) community
|
||||
*/
|
||||
public static create(
|
||||
payload: Uint8Array,
|
||||
type: ApplicationMetadataMessage_Type,
|
||||
identity: Identity
|
||||
): ApplicationMetadataMessage {
|
||||
const signature = identity.sign(payload);
|
||||
|
||||
const proto = {
|
||||
signature,
|
||||
payload,
|
||||
type,
|
||||
};
|
||||
|
||||
return new ApplicationMetadataMessage(proto);
|
||||
}
|
||||
|
||||
static decode(bytes: Uint8Array): ApplicationMetadataMessage {
|
||||
const protoBuf = proto.ApplicationMetadataMessage.decode(
|
||||
Reader.create(bytes)
|
||||
);
|
||||
|
||||
return new ApplicationMetadataMessage(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.ApplicationMetadataMessage.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
public get signature(): Uint8Array | undefined {
|
||||
return this.proto.signature;
|
||||
}
|
||||
|
||||
public get payload(): Uint8Array | undefined {
|
||||
return this.proto.payload;
|
||||
}
|
||||
public get type(): ApplicationMetadataMessage_Type | undefined {
|
||||
return this.proto.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a chat message if the type is [TYPE_CHAT_MESSAGE], undefined otherwise.
|
||||
*/
|
||||
public get chatMessage(): ChatMessage | undefined {
|
||||
if (!this.payload) return;
|
||||
|
||||
return ChatMessage.decode(this.payload);
|
||||
}
|
||||
|
||||
public get signer(): Uint8Array | undefined {
|
||||
if (!this.signature || !this.payload) return;
|
||||
|
||||
const signature = this.signature.slice(0, 64);
|
||||
const recid = this.signature.slice(64)[0];
|
||||
const hash = keccak256(this.payload);
|
||||
|
||||
return secp256k1.ecdsaRecover(signature, recid, hexToBuf(hash));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import * as proto from "../proto/communities/v1/chat_identity";
|
||||
import { IdentityImage } from "../proto/communities/v1/chat_identity";
|
||||
|
||||
export class ChatIdentity {
|
||||
public constructor(public proto: proto.ChatIdentity) {}
|
||||
|
||||
static decode(bytes: Uint8Array): ChatIdentity {
|
||||
const protoBuf = proto.ChatIdentity.decode(Reader.create(bytes));
|
||||
|
||||
return new ChatIdentity(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.ChatIdentity.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
/** Lamport timestamp of the message */
|
||||
get clock(): number | undefined {
|
||||
return this.proto.clock;
|
||||
}
|
||||
|
||||
/** ens_name is the valid ENS name associated with the chat key */
|
||||
get ensName(): string | undefined {
|
||||
return this.proto.ensName;
|
||||
}
|
||||
|
||||
/** images is a string indexed mapping of images associated with an identity */
|
||||
get images(): { [key: string]: IdentityImage } | undefined {
|
||||
return this.proto.images;
|
||||
}
|
||||
|
||||
/** display name is the user set identity, valid only for organisations */
|
||||
get displayName(): string | undefined {
|
||||
return this.proto.displayName;
|
||||
}
|
||||
|
||||
/** description is the user set description, valid only for organisations */
|
||||
get description(): string | undefined {
|
||||
return this.proto.description;
|
||||
}
|
||||
|
||||
get color(): string | undefined {
|
||||
return this.proto.color;
|
||||
}
|
||||
|
||||
get emoji(): string | undefined {
|
||||
return this.proto.emoji;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
AudioMessage_AudioType,
|
||||
ChatMessage_ContentType,
|
||||
} from "../proto/communities/v1/chat_message";
|
||||
import { ImageType } from "../proto/communities/v1/enums";
|
||||
|
||||
import {
|
||||
AudioContent,
|
||||
ChatMessage,
|
||||
ContentType,
|
||||
ImageContent,
|
||||
StickerContent,
|
||||
} from "./chat_message";
|
||||
|
||||
describe("Chat Message", () => {
|
||||
it("Encode & decode Image message", () => {
|
||||
const payload = Buffer.from([1, 1]);
|
||||
|
||||
const imageContent: ImageContent = {
|
||||
image: payload,
|
||||
imageType: ImageType.IMAGE_TYPE_PNG,
|
||||
contentType: ContentType.Image,
|
||||
};
|
||||
|
||||
const message = ChatMessage.createMessage(1, 1, "chat-id", imageContent);
|
||||
|
||||
const buf = message.encode();
|
||||
const dec = ChatMessage.decode(buf);
|
||||
|
||||
expect(dec.contentType).eq(ChatMessage_ContentType.CONTENT_TYPE_IMAGE);
|
||||
expect(dec.image?.payload?.toString()).eq(payload.toString());
|
||||
expect(dec.image?.type).eq(ImageType.IMAGE_TYPE_PNG);
|
||||
});
|
||||
|
||||
it("Encode & decode Audio message", () => {
|
||||
const payload = Buffer.from([1, 1]);
|
||||
const durationMs = 12345;
|
||||
|
||||
const audioContent: AudioContent = {
|
||||
audio: payload,
|
||||
audioType: AudioMessage_AudioType.AUDIO_TYPE_AAC,
|
||||
durationMs,
|
||||
contentType: ContentType.Audio,
|
||||
};
|
||||
|
||||
const message = ChatMessage.createMessage(1, 1, "chat-id", audioContent);
|
||||
|
||||
const buf = message.encode();
|
||||
const dec = ChatMessage.decode(buf);
|
||||
|
||||
expect(dec.contentType).eq(ChatMessage_ContentType.CONTENT_TYPE_AUDIO);
|
||||
expect(dec.audio?.payload?.toString()).eq(payload.toString());
|
||||
expect(dec.audio?.type).eq(ImageType.IMAGE_TYPE_PNG);
|
||||
expect(dec.audio?.durationMs).eq(durationMs);
|
||||
});
|
||||
|
||||
it("Encode & decode Sticker message", () => {
|
||||
const hash = "deadbeef";
|
||||
const pack = 12345;
|
||||
|
||||
const stickerContent: StickerContent = {
|
||||
hash,
|
||||
pack,
|
||||
contentType: ContentType.Sticker,
|
||||
};
|
||||
|
||||
const message = ChatMessage.createMessage(1, 1, "chat-id", stickerContent);
|
||||
|
||||
const buf = message.encode();
|
||||
const dec = ChatMessage.decode(buf);
|
||||
|
||||
expect(dec.contentType).eq(ChatMessage_ContentType.CONTENT_TYPE_STICKER);
|
||||
expect(dec.sticker?.hash).eq(hash);
|
||||
expect(dec.sticker?.pack).eq(pack);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,226 @@
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import * as proto from "../proto/communities/v1/chat_message";
|
||||
import {
|
||||
AudioMessage,
|
||||
AudioMessage_AudioType,
|
||||
ChatMessage_ContentType,
|
||||
ImageMessage,
|
||||
StickerMessage,
|
||||
} from "../proto/communities/v1/chat_message";
|
||||
import { ImageType, MessageType } from "../proto/communities/v1/enums";
|
||||
|
||||
export type Content =
|
||||
| TextContent
|
||||
| StickerContent
|
||||
| ImageContent
|
||||
| AudioContent;
|
||||
|
||||
export enum ContentType {
|
||||
Text,
|
||||
Sticker,
|
||||
Image,
|
||||
Audio,
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
text: string;
|
||||
contentType: ContentType.Text;
|
||||
}
|
||||
|
||||
export interface StickerContent {
|
||||
hash: string;
|
||||
pack: number;
|
||||
contentType: ContentType.Sticker;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
image: Uint8Array;
|
||||
imageType: ImageType;
|
||||
contentType: ContentType.Image;
|
||||
}
|
||||
|
||||
export interface AudioContent {
|
||||
audio: Uint8Array;
|
||||
audioType: AudioMessage_AudioType;
|
||||
durationMs: number;
|
||||
contentType: ContentType.Audio;
|
||||
}
|
||||
|
||||
function isText(content: Content): content is TextContent {
|
||||
return content.contentType === ContentType.Text;
|
||||
}
|
||||
|
||||
function isSticker(content: Content): content is StickerContent {
|
||||
return content.contentType === ContentType.Sticker;
|
||||
}
|
||||
|
||||
function isImage(content: Content): content is ImageContent {
|
||||
return content.contentType === ContentType.Image;
|
||||
}
|
||||
|
||||
function isAudio(content: Content): content is AudioContent {
|
||||
return content.contentType === ContentType.Audio;
|
||||
}
|
||||
|
||||
export class ChatMessage {
|
||||
private constructor(public proto: proto.ChatMessage) {}
|
||||
|
||||
/**
|
||||
* Create a chat message to be sent to an Open (permission = no membership) community.
|
||||
*
|
||||
* @throws string If mediaContent is malformed
|
||||
*/
|
||||
public static createMessage(
|
||||
clock: number,
|
||||
timestamp: number,
|
||||
chatId: string,
|
||||
content: Content,
|
||||
responseTo?: string
|
||||
): ChatMessage {
|
||||
let sticker,
|
||||
image,
|
||||
audio,
|
||||
text = "Upgrade to the latest version to see this media content.";
|
||||
let contentType = ChatMessage_ContentType.CONTENT_TYPE_TEXT_PLAIN;
|
||||
|
||||
if (isText(content)) {
|
||||
if (!content.text) throw "Malformed Text Content";
|
||||
text = content.text;
|
||||
contentType = ChatMessage_ContentType.CONTENT_TYPE_TEXT_PLAIN;
|
||||
} else if (isSticker(content)) {
|
||||
if (!content.hash || !content.pack) throw "Malformed Sticker Content";
|
||||
sticker = {
|
||||
hash: content.hash,
|
||||
pack: content.pack,
|
||||
};
|
||||
contentType = ChatMessage_ContentType.CONTENT_TYPE_STICKER;
|
||||
} else if (isImage(content)) {
|
||||
if (!content.image || !content.imageType) throw "Malformed Image Content";
|
||||
image = {
|
||||
payload: content.image,
|
||||
type: content.imageType,
|
||||
};
|
||||
contentType = ChatMessage_ContentType.CONTENT_TYPE_IMAGE;
|
||||
} else if (isAudio(content)) {
|
||||
if (!content.audio || !content.audioType || !content.durationMs)
|
||||
throw "Malformed Audio Content";
|
||||
audio = {
|
||||
payload: content.audio,
|
||||
type: content.audioType,
|
||||
durationMs: content.durationMs,
|
||||
};
|
||||
contentType = ChatMessage_ContentType.CONTENT_TYPE_AUDIO;
|
||||
}
|
||||
|
||||
const proto = {
|
||||
clock, // ms?
|
||||
timestamp, //ms?
|
||||
text,
|
||||
/** Id of the message that we are replying to */
|
||||
responseTo: responseTo ?? "",
|
||||
/** Ens name of the sender */
|
||||
ensName: "",
|
||||
/** Public Key of the community (TBC) **/
|
||||
chatId,
|
||||
/** The type of message (public/one-to-one/private-group-chat) */
|
||||
messageType: MessageType.MESSAGE_TYPE_COMMUNITY_CHAT,
|
||||
/** The type of the content of the message */
|
||||
contentType,
|
||||
sticker,
|
||||
image,
|
||||
audio,
|
||||
community: undefined, // Used to share a community
|
||||
grant: undefined,
|
||||
};
|
||||
|
||||
return new ChatMessage(proto);
|
||||
}
|
||||
|
||||
static decode(bytes: Uint8Array): ChatMessage {
|
||||
const protoBuf = proto.ChatMessage.decode(Reader.create(bytes));
|
||||
|
||||
return new ChatMessage(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.ChatMessage.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
/** Lamport timestamp of the chat message */
|
||||
public get clock(): number | undefined {
|
||||
return this.proto.clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix timestamps in milliseconds, currently not used as we use whisper as more reliable, but here
|
||||
* so that we don't rely on it
|
||||
*/
|
||||
public get timestamp(): number | undefined {
|
||||
return this.proto.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text of the message
|
||||
*/
|
||||
public get text(): string | undefined {
|
||||
return this.proto.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Id of the message that we are replying to
|
||||
*/
|
||||
public get responseTo(): string | undefined {
|
||||
return this.proto.responseTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ens name of the sender
|
||||
*/
|
||||
public get ensName(): string | undefined {
|
||||
return this.proto.ensName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat id, this field is symmetric for public-chats and private group chats,
|
||||
* but asymmetric in case of one-to-ones, as the sender will use the chat-id
|
||||
* of the received, while the receiver will use the chat-id of the sender.
|
||||
* Probably should be the concatenation of sender-pk & receiver-pk in alphabetical order
|
||||
*/
|
||||
public get chatId(): string {
|
||||
return this.proto.chatId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of message (public/one-to-one/private-group-chat)
|
||||
*/
|
||||
public get messageType(): MessageType | undefined {
|
||||
return this.proto.messageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the content of the message
|
||||
*/
|
||||
public get contentType(): ChatMessage_ContentType | undefined {
|
||||
return this.proto.contentType;
|
||||
}
|
||||
|
||||
public get sticker(): StickerMessage | undefined {
|
||||
return this.proto.sticker;
|
||||
}
|
||||
|
||||
public get image(): ImageMessage | undefined {
|
||||
return this.proto.image;
|
||||
}
|
||||
|
||||
public get audio(): AudioMessage | undefined {
|
||||
return this.proto.audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when sharing a community via a chat message.
|
||||
*/
|
||||
public get community(): Uint8Array | undefined {
|
||||
return this.proto.community;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import * as proto from "../proto/communities/v1/communities";
|
||||
import {
|
||||
CommunityMember,
|
||||
CommunityPermissions,
|
||||
} from "../proto/communities/v1/communities";
|
||||
|
||||
import { ChatIdentity } from "./chat_identity";
|
||||
|
||||
export class CommunityChat {
|
||||
public constructor(public proto: proto.CommunityChat) {}
|
||||
|
||||
/**
|
||||
* Decode the payload as CommunityChat message.
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
static decode(bytes: Uint8Array): CommunityChat {
|
||||
const protoBuf = proto.CommunityChat.decode(Reader.create(bytes));
|
||||
|
||||
return new CommunityChat(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.CommunityChat.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
// TODO: check and document what is the key of the returned Map;
|
||||
public get members(): Map<string, CommunityMember> {
|
||||
const map = new Map();
|
||||
|
||||
for (const key of Object.keys(this.proto.members)) {
|
||||
map.set(key, this.proto.members[key]);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public get permissions(): CommunityPermissions | undefined {
|
||||
return this.proto.permissions;
|
||||
}
|
||||
|
||||
public get identity(): ChatIdentity | undefined {
|
||||
if (!this.proto.identity) return;
|
||||
|
||||
return new ChatIdentity(this.proto.identity);
|
||||
}
|
||||
|
||||
// TODO: Document this
|
||||
public get categoryId(): string | undefined {
|
||||
return this.proto.categoryId;
|
||||
}
|
||||
|
||||
// TODO: Document this
|
||||
public get position(): number | undefined {
|
||||
return this.proto.position;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import debug from "debug";
|
||||
import { WakuMessage, WakuStore } from "js-waku";
|
||||
import { Reader } from "protobufjs";
|
||||
|
||||
import { idToContentTopic } from "../contentTopic";
|
||||
import { createSymKeyFromPassword } from "../encryption";
|
||||
import * as proto from "../proto/communities/v1/communities";
|
||||
import { bufToHex } from "../utils";
|
||||
|
||||
import { ApplicationMetadataMessage } from "./application_metadata_message";
|
||||
import { ChatIdentity } from "./chat_identity";
|
||||
import { CommunityChat } from "./community_chat";
|
||||
|
||||
const dbg = debug("communities:wire:community_description");
|
||||
|
||||
export class CommunityDescription {
|
||||
private constructor(public proto: proto.CommunityDescription) {}
|
||||
|
||||
static decode(bytes: Uint8Array): CommunityDescription {
|
||||
const protoBuf = proto.CommunityDescription.decode(Reader.create(bytes));
|
||||
|
||||
return new CommunityDescription(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.CommunityDescription.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the most recent Community Description it can find on the network.
|
||||
*/
|
||||
public static async retrieve(
|
||||
communityPublicKey: Uint8Array,
|
||||
wakuStore: WakuStore
|
||||
): Promise<CommunityDescription | undefined> {
|
||||
const hexCommunityPublicKey = bufToHex(communityPublicKey);
|
||||
const contentTopic = idToContentTopic(hexCommunityPublicKey);
|
||||
|
||||
let communityDescription: CommunityDescription | undefined;
|
||||
|
||||
const callback = (messages: WakuMessage[]): void => {
|
||||
// Value found, stop processing
|
||||
if (communityDescription) return;
|
||||
|
||||
// Process most recent message first
|
||||
const orderedMessages = messages.reverse();
|
||||
orderedMessages.forEach((message: WakuMessage) => {
|
||||
if (!message.payload) return;
|
||||
try {
|
||||
const metadata = ApplicationMetadataMessage.decode(message.payload);
|
||||
if (!metadata.payload) return;
|
||||
|
||||
const _communityDescription = CommunityDescription.decode(
|
||||
metadata.payload
|
||||
);
|
||||
|
||||
if (!_communityDescription.identity) return;
|
||||
|
||||
communityDescription = _communityDescription;
|
||||
} catch (e) {
|
||||
dbg(
|
||||
`Failed to decode message as CommunityDescription found on content topic ${contentTopic}`,
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const symKey = await createSymKeyFromPassword(hexCommunityPublicKey);
|
||||
|
||||
await wakuStore
|
||||
.queryHistory([contentTopic], {
|
||||
callback,
|
||||
decryptionKeys: [symKey],
|
||||
})
|
||||
.catch((e) => {
|
||||
dbg(
|
||||
`Failed to retrieve community description for ${hexCommunityPublicKey}`,
|
||||
e
|
||||
);
|
||||
});
|
||||
|
||||
return communityDescription;
|
||||
}
|
||||
|
||||
get identity(): ChatIdentity | undefined {
|
||||
if (!this.proto.identity) return;
|
||||
|
||||
return new ChatIdentity(this.proto.identity);
|
||||
}
|
||||
|
||||
get chats(): Map<string, CommunityChat> {
|
||||
const map = new Map();
|
||||
|
||||
for (const key of Object.keys(this.proto.chats)) {
|
||||
map.set(key, this.proto.chats[key]);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
import { keccak256 } from "js-sha3";
|
||||
import { Reader } from "protobufjs";
|
||||
import * as secp256k1 from "secp256k1";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Identity } from "..";
|
||||
import * as proto from "../proto/communities/v1/membership_update_message";
|
||||
import { bufToHex, hexToBuf } from "../utils";
|
||||
|
||||
export class MembershipUpdateEvent {
|
||||
public constructor(public proto: proto.MembershipUpdateEvent) {}
|
||||
|
||||
static decode(bytes: Uint8Array): MembershipUpdateEvent {
|
||||
const protoBuf = proto.MembershipUpdateEvent.decode(Reader.create(bytes));
|
||||
return new MembershipUpdateEvent(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.MembershipUpdateEvent.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
public get members(): string[] {
|
||||
return this.proto.members;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this.proto.name;
|
||||
}
|
||||
|
||||
public get clock(): number {
|
||||
return this.proto.clock;
|
||||
}
|
||||
|
||||
public get type(): proto.MembershipUpdateEvent_EventType {
|
||||
return this.proto.type;
|
||||
}
|
||||
}
|
||||
|
||||
export class MembershipSignedEvent {
|
||||
public sig: Uint8Array;
|
||||
public event: MembershipUpdateEvent;
|
||||
private chatId: string;
|
||||
|
||||
public constructor(
|
||||
sig: Uint8Array,
|
||||
event: MembershipUpdateEvent,
|
||||
chatId: string
|
||||
) {
|
||||
this.sig = sig;
|
||||
this.event = event;
|
||||
this.chatId = chatId;
|
||||
}
|
||||
|
||||
public get signer(): Uint8Array | undefined {
|
||||
const encEvent = this.event.encode();
|
||||
const eventToSign = Buffer.concat([hexToBuf(this.chatId), encEvent]);
|
||||
|
||||
if (!this.sig || !eventToSign) return;
|
||||
|
||||
const signature = this.sig.slice(0, 64);
|
||||
const recid = this.sig.slice(64)[0];
|
||||
const hash = keccak256(eventToSign);
|
||||
|
||||
return secp256k1.ecdsaRecover(signature, recid, hexToBuf(hash));
|
||||
}
|
||||
}
|
||||
|
||||
export class MembershipUpdateMessage {
|
||||
private clock: number = Date.now();
|
||||
private identity: Identity = Identity.generate();
|
||||
public constructor(public proto: proto.MembershipUpdateMessage) {}
|
||||
|
||||
public static create(
|
||||
chatId: string,
|
||||
identity: Identity
|
||||
): MembershipUpdateMessage {
|
||||
const partial = proto.MembershipUpdateMessage.fromPartial({
|
||||
chatId,
|
||||
events: [],
|
||||
});
|
||||
const newMessage = new MembershipUpdateMessage(partial);
|
||||
newMessage.clock = Date.now();
|
||||
newMessage.identity = identity;
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
private addEvent(event: MembershipUpdateEvent): void {
|
||||
const encEvent = event.encode();
|
||||
const eventToSign = Buffer.concat([hexToBuf(this.proto.chatId), encEvent]);
|
||||
const signature = this.identity.sign(eventToSign);
|
||||
this.proto.events.push(Buffer.concat([signature, encEvent]));
|
||||
}
|
||||
|
||||
public static createChat(
|
||||
identity: Identity,
|
||||
members: string[],
|
||||
name?: string
|
||||
): MembershipUpdateMessage {
|
||||
const chatId = `${uuidV4()}-${bufToHex(identity.publicKey)}`;
|
||||
|
||||
const message = this.create(chatId, identity);
|
||||
const type = proto.MembershipUpdateEvent_EventType.CHAT_CREATED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: message.clock,
|
||||
members,
|
||||
name: name ?? "",
|
||||
type,
|
||||
});
|
||||
message.addEvent(event);
|
||||
return message;
|
||||
}
|
||||
|
||||
public addNameChangeEvent(name: string): void {
|
||||
const type = proto.MembershipUpdateEvent_EventType.NAME_CHANGED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: this.clock,
|
||||
members: [],
|
||||
name: name,
|
||||
type,
|
||||
});
|
||||
this.addEvent(event);
|
||||
}
|
||||
|
||||
public addMembersAddedEvent(members: string[]): void {
|
||||
const type = proto.MembershipUpdateEvent_EventType.MEMBERS_ADDED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: this.clock,
|
||||
members,
|
||||
name: "",
|
||||
type,
|
||||
});
|
||||
this.addEvent(event);
|
||||
}
|
||||
|
||||
public addMemberJoinedEvent(member: string): void {
|
||||
const type = proto.MembershipUpdateEvent_EventType.MEMBER_JOINED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: this.clock,
|
||||
members: [member],
|
||||
name: "",
|
||||
type,
|
||||
});
|
||||
this.addEvent(event);
|
||||
}
|
||||
|
||||
public addMemberRemovedEvent(member: string): void {
|
||||
const type = proto.MembershipUpdateEvent_EventType.MEMBER_REMOVED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: this.clock,
|
||||
members: [member],
|
||||
name: "",
|
||||
type,
|
||||
});
|
||||
this.addEvent(event);
|
||||
}
|
||||
|
||||
public addAdminsAddedEvent(members: string[]): void {
|
||||
const type = proto.MembershipUpdateEvent_EventType.ADMINS_ADDED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: this.clock,
|
||||
members,
|
||||
name: "",
|
||||
type,
|
||||
});
|
||||
this.addEvent(event);
|
||||
}
|
||||
|
||||
public addAdminRemovedEvent(member: string): void {
|
||||
const type = proto.MembershipUpdateEvent_EventType.ADMINS_ADDED;
|
||||
const event = new MembershipUpdateEvent({
|
||||
clock: this.clock,
|
||||
members: [member],
|
||||
name: "",
|
||||
type,
|
||||
});
|
||||
this.addEvent(event);
|
||||
}
|
||||
|
||||
static decode(bytes: Uint8Array): MembershipUpdateMessage {
|
||||
const protoBuf = proto.MembershipUpdateMessage.decode(Reader.create(bytes));
|
||||
return new MembershipUpdateMessage(protoBuf);
|
||||
}
|
||||
|
||||
public get events(): MembershipSignedEvent[] {
|
||||
return this.proto.events.map((bufArray) => {
|
||||
return new MembershipSignedEvent(
|
||||
bufArray.slice(0, 65),
|
||||
MembershipUpdateEvent.decode(bufArray.slice(65)),
|
||||
this.chatId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public get chatId(): string {
|
||||
return this.proto.chatId;
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.MembershipUpdateMessage.encode(this.proto).finish();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import * as proto from "../proto/communities/v1/status_update";
|
||||
|
||||
export class StatusUpdate {
|
||||
public constructor(public proto: proto.StatusUpdate) {}
|
||||
|
||||
public static create(
|
||||
statusType: proto.StatusUpdate_StatusType,
|
||||
customText: string
|
||||
): StatusUpdate {
|
||||
const clock = Date.now();
|
||||
|
||||
const proto = {
|
||||
clock,
|
||||
statusType,
|
||||
customText,
|
||||
};
|
||||
|
||||
return new StatusUpdate(proto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the payload as CommunityChat message.
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
static decode(bytes: Uint8Array): StatusUpdate {
|
||||
const protoBuf = proto.StatusUpdate.decode(Reader.create(bytes));
|
||||
|
||||
return new StatusUpdate(protoBuf);
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return proto.StatusUpdate.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
public get clock(): number | undefined {
|
||||
return this.proto.clock;
|
||||
}
|
||||
|
||||
public get statusType(): proto.StatusUpdate_StatusType | undefined {
|
||||
return this.proto.statusType;
|
||||
}
|
||||
|
||||
public get customText(): string | undefined {
|
||||
return this.proto.customText;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"target": "es6",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"composite": true,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": false /* to set at a later stage */,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Debugging Options */
|
||||
"traceResolution": false,
|
||||
"listEmittedFiles": false,
|
||||
"listFiles": false,
|
||||
"pretty": true,
|
||||
|
||||
// Due to broken types in indirect dependencies
|
||||
"skipLibCheck": true,
|
||||
|
||||
"lib": ["es6", "dom"],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./src/types",
|
||||
"../../node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules/**"],
|
||||
"types": ["mocha"],
|
||||
"compileOnSave": false
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "project": "./tsconfig.json" },
|
||||
"env": { "es6": true },
|
||||
"ignorePatterns": ["node_modules", "dist", "coverage", "proto"],
|
||||
"plugins": ["import", "eslint-comments", "functional"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"eslint-comments/disable-enable-pair": [
|
||||
"error",
|
||||
{ "allowWholeFile": true }
|
||||
],
|
||||
"eslint-comments/no-unused-disable": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
|
||||
],
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true }
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.spec.ts", "**/test_utils/*.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/*.spec.ts",
|
||||
"require": "ts-node/register",
|
||||
"exit": true
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# package.json is formatted by package managers, so we ignore it here
|
||||
package.json
|
|
@ -0,0 +1 @@
|
|||
# `status-react`
|
|
@ -0,0 +1,6 @@
|
|||
version: v1beta1
|
||||
|
||||
plugins:
|
||||
- name: ts_proto
|
||||
out: ./src/proto
|
||||
opt: grpc_js,esModuleInterop=true
|
|
@ -0,0 +1,5 @@
|
|||
version: v1beta1
|
||||
|
||||
build:
|
||||
roots:
|
||||
- ./proto
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "@status-im/react",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"homepage": "https://github.com/status-im/status-web",
|
||||
"repository": {
|
||||
"url": "https://github.com/status-im/status-web.git",
|
||||
"directory": "packages/status-react",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/status-im/status-web/issues"
|
||||
},
|
||||
"main": "dist/cjs/src/index.js",
|
||||
"module": "dist/esm/src/index.js",
|
||||
"types": "dist/esm/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "run-s 'build:*'",
|
||||
"build:esm": "tsc --module es2020 --target es2017 --outDir dist/esm",
|
||||
"build:cjs": "tsc --outDir dist/cjs",
|
||||
"fix": "run-s 'fix:*'",
|
||||
"fix:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --write",
|
||||
"fix:lint": "eslint './{src,test}/**/*.{ts,tsx}' --fix",
|
||||
"test": "run-s 'test:*'",
|
||||
"test:lint": "eslint './{src,test}/**/*.{ts,tsx}'",
|
||||
"test:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --list-different",
|
||||
"proto": "run-s 'proto:*'",
|
||||
"proto:lint": "buf lint",
|
||||
"proto:build": "buf generate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.0.0",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/emoji-mart": "^3.0.6",
|
||||
"@types/hcaptcha__react-hcaptcha": "^0.1.5",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.9.6",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^17.0.16",
|
||||
"@types/styled-components": "^5.1.12",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"chai": "^4.3.4",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"jsdom": "^16.7.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"mocha": "^9.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"npm-watch": "^0.11.0",
|
||||
"prettier": "^2.3.2",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@status-im/core": "^0.0.0",
|
||||
"emoji-mart": "^3.0.1",
|
||||
"html-entities": "^2.3.2",
|
||||
"js-sha3": "^0.8.0",
|
||||
"js-waku": "^0.16.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"styled-components": "^5.3.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import React, { useMemo, useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useIdentity } from "../../contexts/identityProvider";
|
||||
import { useActivities } from "../../hooks/useActivities";
|
||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||
import { TopBtn } from "../Chat/ChatTopbar";
|
||||
import { ActivityIcon } from "../Icons/ActivityIcon";
|
||||
|
||||
import { ActivityCenter } from "./ActivityCenter";
|
||||
|
||||
interface ActivityButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ActivityButton({ className }: ActivityButtonProps) {
|
||||
const { activities, activityDispatch } = useActivities();
|
||||
const identity = useIdentity();
|
||||
const disabled = useMemo(() => !identity, [identity]);
|
||||
const ref = useRef(null);
|
||||
useClickOutside(ref, () => setShowActivityCenter(false));
|
||||
|
||||
const [showActivityCenter, setShowActivityCenter] = useState(false);
|
||||
const badgeAmount = useMemo(
|
||||
() => activities.filter((activity) => !activity.isRead).length,
|
||||
[activities]
|
||||
);
|
||||
|
||||
return (
|
||||
<ActivityWrapper ref={ref} className={className}>
|
||||
<TopBtn
|
||||
onClick={() => setShowActivityCenter(!showActivityCenter)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ActivityIcon />
|
||||
{badgeAmount > 0 && (
|
||||
<NotificationBagde
|
||||
className={
|
||||
badgeAmount > 99
|
||||
? "countless"
|
||||
: badgeAmount > 9
|
||||
? "wide"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{badgeAmount < 100 ? badgeAmount : "∞"}
|
||||
</NotificationBagde>
|
||||
)}
|
||||
</TopBtn>
|
||||
{showActivityCenter && (
|
||||
<ActivityCenter
|
||||
activities={activities}
|
||||
setShowActivityCenter={setShowActivityCenter}
|
||||
activityDispatch={activityDispatch}
|
||||
/>
|
||||
)}
|
||||
</ActivityWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const ActivityWrapper = styled.div`
|
||||
padding-left: 10px;
|
||||
margin-left: 10px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 1px;
|
||||
background: ${({ theme }) => theme.primary};
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
&.creation {
|
||||
padding-left: 0px;
|
||||
margin-left: 16px;
|
||||
|
||||
&:before {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const NotificationBagde = styled.div`
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ theme }) => theme.notificationColor};
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
border-radius: 9px;
|
||||
|
||||
&.wide {
|
||||
width: 26px;
|
||||
right: -7px;
|
||||
}
|
||||
|
||||
&.countless {
|
||||
width: 22px;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,189 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { ActivityAction } from "../../hooks/useActivities";
|
||||
import { Activity } from "../../models/Activity";
|
||||
import { buttonTransparentStyles } from "../Buttons/buttonStyle";
|
||||
import { Tooltip } from "../Form/Tooltip";
|
||||
import { HideIcon } from "../Icons/HideIcon";
|
||||
import { ReadIcon } from "../Icons/ReadIcon";
|
||||
import { ShowIcon } from "../Icons/ShowIcon";
|
||||
|
||||
import { ActivityMessage } from "./ActivityMessage";
|
||||
|
||||
interface ActivityCenterProps {
|
||||
activities: Activity[];
|
||||
setShowActivityCenter: (val: boolean) => void;
|
||||
activityDispatch: React.Dispatch<ActivityAction>;
|
||||
}
|
||||
|
||||
export function ActivityCenter({
|
||||
activities,
|
||||
setShowActivityCenter,
|
||||
activityDispatch,
|
||||
}: ActivityCenterProps) {
|
||||
const { contacts } = useMessengerContext();
|
||||
|
||||
const shownActivities = useMemo(
|
||||
() =>
|
||||
activities.filter(
|
||||
(activity) => !contacts?.[activity.user]?.blocked ?? true
|
||||
),
|
||||
[contacts, activities]
|
||||
);
|
||||
|
||||
const [hideRead, setHideRead] = useState(false);
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const filteredActivities = shownActivities.filter((activity) =>
|
||||
filter
|
||||
? activity.type === filter
|
||||
: hideRead
|
||||
? activity.isRead !== true
|
||||
: activity
|
||||
);
|
||||
|
||||
return (
|
||||
<ActivityBlock>
|
||||
<ActivityFilter>
|
||||
<FlexDiv>
|
||||
<FilterBtn onClick={() => setFilter("")}>All</FilterBtn>
|
||||
<FilterBtn onClick={() => setFilter("mention")}>Mentions</FilterBtn>
|
||||
<FilterBtn onClick={() => setFilter("reply")}>Replies</FilterBtn>
|
||||
<FilterBtn onClick={() => setFilter("request")}>
|
||||
Contact requests
|
||||
</FilterBtn>
|
||||
</FlexDiv>
|
||||
<Btns>
|
||||
<BtnWrapper>
|
||||
<ActivityBtn
|
||||
onClick={() => activityDispatch({ type: "setAllAsRead" })}
|
||||
>
|
||||
<ReadIcon />
|
||||
</ActivityBtn>
|
||||
<Tooltip tip="Mark all as Read" />
|
||||
</BtnWrapper>
|
||||
<BtnWrapper>
|
||||
<ActivityBtn onClick={() => setHideRead(!hideRead)}>
|
||||
{hideRead ? <ShowIcon /> : <HideIcon />}
|
||||
</ActivityBtn>
|
||||
<Tooltip tip={hideRead ? "Show read" : "Hide read"} />
|
||||
</BtnWrapper>
|
||||
</Btns>
|
||||
</ActivityFilter>
|
||||
{filteredActivities.length > 0 ? (
|
||||
<Activities>
|
||||
{filteredActivities.map((activity) => (
|
||||
<ActivityMessage
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
setShowActivityCenter={setShowActivityCenter}
|
||||
activityDispatch={activityDispatch}
|
||||
/>
|
||||
))}
|
||||
</Activities>
|
||||
) : (
|
||||
<EmptyActivities>Notifications will appear here</EmptyActivities>
|
||||
)}
|
||||
</ActivityBlock>
|
||||
);
|
||||
}
|
||||
|
||||
const ActivityBlock = styled.div`
|
||||
width: 600px;
|
||||
height: 770px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
box-shadow: 0px 12px 24px rgba(0, 34, 51, 0.1);
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const ActivityFilter = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 13px 16px;
|
||||
`;
|
||||
|
||||
export const FlexDiv = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const FilterBtn = styled.button`
|
||||
${buttonTransparentStyles}
|
||||
|
||||
& + & {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const BtnWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActivityBtn = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
align-self: center;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&.read {
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
}
|
||||
|
||||
&.accept {
|
||||
&:hover {
|
||||
background: rgba(78, 188, 96, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.decline {
|
||||
&:hover {
|
||||
background: rgba(255, 45, 85, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Activities = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const EmptyActivities = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
`;
|
||||
|
||||
const Btns = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
|
@ -0,0 +1,386 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useModal } from "../../contexts/modalProvider";
|
||||
import { useScrollToMessage } from "../../contexts/scrollProvider";
|
||||
import { ActivityAction } from "../../hooks/useActivities";
|
||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||
import { Activity } from "../../models/Activity";
|
||||
import { equalDate } from "../../utils/equalDate";
|
||||
import { DownloadButton } from "../Buttons/DownloadButton";
|
||||
import { Mention } from "../Chat/ChatMessageContent";
|
||||
import { Logo } from "../CommunityIdentity";
|
||||
import { ContactMenu } from "../Form/ContactMenu";
|
||||
import { Tooltip } from "../Form/Tooltip";
|
||||
import { CheckIcon } from "../Icons/CheckIcon";
|
||||
import { ClearSvg } from "../Icons/ClearIcon";
|
||||
import { CommunityIcon } from "../Icons/CommunityIcon";
|
||||
import { GroupIcon } from "../Icons/GroupIcon";
|
||||
import { MoreIcon } from "../Icons/MoreIcon";
|
||||
import { ReadMessageIcon } from "../Icons/ReadMessageIcon";
|
||||
import { ReplyIcon } from "../Icons/ReplyActivityIcon";
|
||||
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
|
||||
import { UserIcon } from "../Icons/UserIcon";
|
||||
import {
|
||||
ContentWrapper,
|
||||
DateSeparator,
|
||||
MessageHeaderWrapper,
|
||||
MessageOuterWrapper,
|
||||
MessageText,
|
||||
TimeWrapper,
|
||||
UserAddress,
|
||||
UserName,
|
||||
UserNameWrapper,
|
||||
} from "../Messages/Styles";
|
||||
import { ProfileModalName } from "../Modals/ProfileModal";
|
||||
import { textMediumStyles, textSmallStyles } from "../Text";
|
||||
|
||||
import { ActivityBtn, FlexDiv } from "./ActivityCenter";
|
||||
|
||||
const today = new Date();
|
||||
|
||||
type ActivityMessageProps = {
|
||||
activity: Activity;
|
||||
setShowActivityCenter: (val: boolean) => void;
|
||||
activityDispatch: React.Dispatch<ActivityAction>;
|
||||
};
|
||||
|
||||
export function ActivityMessage({
|
||||
activity,
|
||||
setShowActivityCenter,
|
||||
activityDispatch,
|
||||
}: ActivityMessageProps) {
|
||||
const { contacts, channelsDispatch } = useMessengerContext();
|
||||
const scroll = useScrollToMessage();
|
||||
const { setModal } = useModal(ProfileModalName);
|
||||
const showChannel = () => {
|
||||
"channel" in activity &&
|
||||
channelsDispatch({ type: "ChangeActive", payload: activity.channel.id }),
|
||||
setShowActivityCenter(false);
|
||||
};
|
||||
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
const type = activity.type;
|
||||
|
||||
const contact = useMemo(
|
||||
() => contacts[activity.user],
|
||||
[activity.user, contacts]
|
||||
);
|
||||
|
||||
const [elements, setElements] = useState<
|
||||
(string | React.ReactElement | undefined)[]
|
||||
>(["message" in activity ? activity.message?.content : undefined]);
|
||||
|
||||
useEffect(() => {
|
||||
if ("message" in activity) {
|
||||
const split = activity.message?.content.split(" ");
|
||||
const newSplit = split.flatMap((element, idx) => {
|
||||
if (element.startsWith("@")) {
|
||||
return [
|
||||
<Mention
|
||||
key={idx}
|
||||
id={element}
|
||||
setMentioned={() => true}
|
||||
className="activity"
|
||||
/>,
|
||||
" ",
|
||||
];
|
||||
}
|
||||
return [element, " "];
|
||||
});
|
||||
newSplit.pop();
|
||||
setElements(newSplit);
|
||||
}
|
||||
}, [activity]);
|
||||
|
||||
const ref = useRef(null);
|
||||
useClickOutside(ref, () => setShowMenu(false));
|
||||
|
||||
return (
|
||||
<MessageOuterWrapper>
|
||||
<ActivityDate>
|
||||
{equalDate(activity.date, today)
|
||||
? "Today"
|
||||
: activity.date.toLocaleDateString()}
|
||||
</ActivityDate>
|
||||
|
||||
<MessageWrapper className={`${!activity.isRead && "unread"}`}>
|
||||
<>
|
||||
<UserIcon />
|
||||
<ActivityContent>
|
||||
<MessageHeaderWrapper>
|
||||
<UserNameWrapper>
|
||||
<ActivityUserName
|
||||
onClick={() => {
|
||||
setModal({
|
||||
id: activity.user,
|
||||
renamingState: false,
|
||||
requestState: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{contact?.customName ?? activity.user.slice(0, 10)}
|
||||
</ActivityUserName>
|
||||
{contact?.customName && (
|
||||
<UserAddress>
|
||||
{activity.user.slice(0, 5)}...{activity.user.slice(-3)}
|
||||
</UserAddress>
|
||||
)}
|
||||
{contact.isUntrustworthy && <UntrustworthIcon />}
|
||||
</UserNameWrapper>
|
||||
<TimeWrapper>
|
||||
{activity.date.toLocaleString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
})}
|
||||
</TimeWrapper>
|
||||
</MessageHeaderWrapper>
|
||||
{type === "request" && (
|
||||
<ContextHeading>
|
||||
Contact request
|
||||
{activity.requestType === "outcome"
|
||||
? ` to ${activity.user.slice(0, 10)}`
|
||||
: ": "}
|
||||
</ContextHeading>
|
||||
)}
|
||||
{type === "invitation" && (
|
||||
<FlexDiv>
|
||||
<ContextHeading>{`Invited you to join a community `}</ContextHeading>
|
||||
<Tag>
|
||||
<CommunityIcon width={17} height={16} />
|
||||
<CommunityLogo
|
||||
style={{
|
||||
backgroundImage: activity.invitation?.icon
|
||||
? `url(${activity.invitation?.icon}`
|
||||
: "",
|
||||
}}
|
||||
>
|
||||
{activity.invitation?.icon === undefined &&
|
||||
activity.invitation?.name.slice(0, 1).toUpperCase()}
|
||||
</CommunityLogo>
|
||||
<span>{activity.invitation?.name}</span>
|
||||
</Tag>
|
||||
</FlexDiv>
|
||||
)}
|
||||
<ActivityText>
|
||||
{"message" in activity && activity.message?.content && (
|
||||
<div
|
||||
onClick={() => {
|
||||
scroll(activity.message, activity.channel.id);
|
||||
setShowActivityCenter(false);
|
||||
}}
|
||||
>
|
||||
{elements.map((el) => el)}
|
||||
</div>
|
||||
)}
|
||||
{activity.type === "request" &&
|
||||
activity.requestType === "income" &&
|
||||
activity.request}
|
||||
</ActivityText>
|
||||
{type === "mention" &&
|
||||
activity.channel &&
|
||||
activity.channel.type !== "dm" && (
|
||||
<Tag onClick={showChannel}>
|
||||
{activity.channel.type === "group" ? <GroupIcon /> : "#"}{" "}
|
||||
<span>{` ${activity.channel.name.slice(0, 10)}`}</span>
|
||||
</Tag>
|
||||
)}
|
||||
{type === "reply" && activity.quote && (
|
||||
<ReplyWrapper>
|
||||
{activity.quote.image && (
|
||||
<ContextHeading>Posted an image in</ContextHeading>
|
||||
)}
|
||||
<Tag onClick={showChannel}>
|
||||
<ReplyIcon /> <span>{activity.quote.content}</span>
|
||||
</Tag>
|
||||
</ReplyWrapper>
|
||||
)}
|
||||
{type === "invitation" && (
|
||||
<InviteDiv>
|
||||
<ContextHeading>{`To access other communities, `}</ContextHeading>
|
||||
<DownloadButton className="activity" />
|
||||
</InviteDiv>
|
||||
)}
|
||||
</ActivityContent>
|
||||
</>
|
||||
{type === "request" &&
|
||||
!activity.status &&
|
||||
activity.requestType === "income" && (
|
||||
<>
|
||||
<ActivityBtn
|
||||
onClick={() => {
|
||||
activityDispatch({
|
||||
type: "setStatus",
|
||||
payload: { id: activity.id, status: "accepted" },
|
||||
});
|
||||
}}
|
||||
className="accept"
|
||||
>
|
||||
<CheckIcon width={20} height={20} className="accept" />
|
||||
</ActivityBtn>
|
||||
<ActivityBtn
|
||||
onClick={() => {
|
||||
activityDispatch({
|
||||
type: "setStatus",
|
||||
payload: { id: activity.id, status: "declined" },
|
||||
});
|
||||
}}
|
||||
className="decline"
|
||||
>
|
||||
<ClearSvg width={20} height={20} className="decline" />
|
||||
</ActivityBtn>
|
||||
<ActivityBtn
|
||||
onClick={() => {
|
||||
setShowMenu((e) => !e);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{showMenu && (
|
||||
<ContactMenu id={activity.user} setShowMenu={setShowMenu} />
|
||||
)}
|
||||
<MoreIcon />
|
||||
</ActivityBtn>
|
||||
</>
|
||||
)}
|
||||
{type === "request" && activity.status === "accepted" && (
|
||||
<RequestStatus className="accepted">Accepted</RequestStatus>
|
||||
)}
|
||||
{type === "request" && activity.status === "declined" && (
|
||||
<RequestStatus className="declined">Declined</RequestStatus>
|
||||
)}
|
||||
{type === "request" && activity.status === "sent" && (
|
||||
<RequestStatus>Sent</RequestStatus>
|
||||
)}
|
||||
{(type === "mention" || type === "reply") && (
|
||||
<BtnWrapper>
|
||||
<ActivityBtn
|
||||
onClick={() =>
|
||||
activityDispatch({ type: "setAsRead", payload: activity.id })
|
||||
}
|
||||
className={`${activity.isRead && "read"}`}
|
||||
>
|
||||
<ReadMessageIcon isRead={activity.isRead} />
|
||||
</ActivityBtn>
|
||||
<Tooltip tip="Mark Read" className="read" />
|
||||
</BtnWrapper>
|
||||
)}
|
||||
</MessageWrapper>
|
||||
</MessageOuterWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const InviteDiv = styled.div`
|
||||
display: flex;
|
||||
margin-top: -4px;
|
||||
`;
|
||||
|
||||
const BtnWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActivityDate = styled(DateSeparator)`
|
||||
justify-content: flex-start;
|
||||
padding: 8px 16px;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const MessageWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px 16px;
|
||||
|
||||
&.unread {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
`;
|
||||
|
||||
const ActivityText = styled(MessageText)`
|
||||
white-space: unset;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const Tag = styled.div`
|
||||
width: fit-content;
|
||||
max-width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.secondary};
|
||||
border-radius: 11px;
|
||||
padding: 0 6px;
|
||||
cursor: pointer;
|
||||
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
${textSmallStyles}
|
||||
|
||||
& > span {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContextHeading = styled.p`
|
||||
font-style: italic;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
flex-shrink: 0;
|
||||
white-space: pre-wrap;
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
const RequestStatus = styled.p`
|
||||
font-weight: 500;
|
||||
align-self: center;
|
||||
text-align: end;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
${textSmallStyles}
|
||||
|
||||
&.accepted {
|
||||
color: ${({ theme }) => theme.greenColor};
|
||||
}
|
||||
|
||||
&.declined {
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
`;
|
||||
|
||||
const ActivityContent = styled(ContentWrapper)`
|
||||
max-width: calc(100% - 80px);
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const ActivityUserName = styled(UserName)`
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const ReplyWrapper = styled.div`
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > p {
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const CommunityLogo = styled(Logo)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 2px 0 4px;
|
||||
|
||||
${textSmallStyles}
|
||||
`;
|
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { LeftIcon } from "../Icons/LeftIcon";
|
||||
|
||||
interface BackButtonProps {
|
||||
onBtnClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function BackButton({ onBtnClick, className }: BackButtonProps) {
|
||||
return (
|
||||
<BackBtn onClick={onBtnClick} className={className}>
|
||||
<LeftIcon width={24} height={24} className="black" />
|
||||
</BackBtn>
|
||||
);
|
||||
}
|
||||
|
||||
const BackBtn = styled.button`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
width: 32px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
|
||||
&.narrow {
|
||||
position: static;
|
||||
margin-right: 13px;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,84 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { buttonStyles } from "./buttonStyle";
|
||||
|
||||
const userAgent = window.navigator.userAgent;
|
||||
const platform = window.navigator.platform;
|
||||
const macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"];
|
||||
const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
|
||||
const iosPlatforms = ["iPhone", "iPad", "iPod"];
|
||||
|
||||
interface DownloadButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DownloadButton = ({ className }: DownloadButtonProps) => {
|
||||
const [link, setlink] = useState("https://status.im/get/");
|
||||
const [os, setOs] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (macosPlatforms.includes(platform)) {
|
||||
setlink(
|
||||
"https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.dmg"
|
||||
);
|
||||
setOs("Mac");
|
||||
} else if (iosPlatforms.includes(platform)) {
|
||||
setlink(
|
||||
"https://apps.apple.com/us/app/status-private-communication/id1178893006"
|
||||
);
|
||||
setOs("iOS");
|
||||
} else if (windowsPlatforms.includes(platform)) {
|
||||
setlink(
|
||||
"https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.exe"
|
||||
);
|
||||
setOs("Windows");
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
setlink(
|
||||
"https://play.google.com/store/apps/details?id=im.status.ethereum"
|
||||
);
|
||||
setOs("Android");
|
||||
} else if (/Linux/.test(platform)) {
|
||||
setlink(
|
||||
"https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.tar.gz"
|
||||
);
|
||||
setOs("Linux");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={className}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{os
|
||||
? `${className === "activity" ? "d" : "D"}ownload Status for ${os}`
|
||||
: `${className === "activity" ? "d" : "D"}ownload Status`}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const Link = styled.a`
|
||||
margin-top: 24px;
|
||||
padding: 11px 32px;
|
||||
|
||||
${buttonStyles}
|
||||
|
||||
&.activity {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
font-style: italic;
|
||||
border-radius: 0;
|
||||
font-weight: 400;
|
||||
text-decoration: underline;
|
||||
background: inherit;
|
||||
|
||||
&:hover {
|
||||
background: inherit;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,59 @@
|
|||
import styled, { css } from "styled-components";
|
||||
|
||||
export const buttonStyles = css`
|
||||
font-family: "Inter";
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export const buttonTransparentStyles = css`
|
||||
font-family: "Inter";
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: ${({ theme }) => theme.tertiary};
|
||||
background: inherit;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: ${({ theme }) => theme.buttonBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonNo = styled.button`
|
||||
padding: 11px 24px;
|
||||
margin-right: 16px;
|
||||
|
||||
${buttonStyles}
|
||||
background: ${({ theme }) => theme.buttonNoBg};
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonNoBgHover};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonYes = styled.button`
|
||||
padding: 11px 24px;
|
||||
|
||||
${buttonStyles}
|
||||
`;
|
|
@ -0,0 +1,211 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { ChannelData } from "../../models/ChannelData";
|
||||
import { ChannelMenu } from "../Form/ChannelMenu";
|
||||
import { Tooltip } from "../Form/Tooltip";
|
||||
import { GroupIcon } from "../Icons/GroupIcon";
|
||||
import { MutedIcon } from "../Icons/MutedIcon";
|
||||
import { textMediumStyles } from "../Text";
|
||||
|
||||
import { ChannelIcon } from "./ChannelIcon";
|
||||
|
||||
function RenderChannelName({
|
||||
channel,
|
||||
activeView,
|
||||
className,
|
||||
}: {
|
||||
channel: ChannelData;
|
||||
activeView?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { activeChannel } = useMessengerContext();
|
||||
switch (channel.type) {
|
||||
case "group":
|
||||
return (
|
||||
<div className={className}>
|
||||
{!activeView && (
|
||||
<GroupIcon active={channel.id === activeChannel?.id} />
|
||||
)}
|
||||
{` ${channel.name}`}
|
||||
</div>
|
||||
);
|
||||
case "channel":
|
||||
return <div className={className}>{`# ${channel.name}`}</div>;
|
||||
case "dm":
|
||||
return <div className={className}>{channel.name.slice(0, 20)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelProps {
|
||||
channel: ChannelData;
|
||||
notified?: boolean;
|
||||
mention?: number;
|
||||
isActive: boolean;
|
||||
activeView?: boolean;
|
||||
onClick?: () => void;
|
||||
setEditGroup?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export function Channel({
|
||||
channel,
|
||||
isActive,
|
||||
activeView,
|
||||
onClick,
|
||||
notified,
|
||||
mention,
|
||||
setEditGroup,
|
||||
}: ChannelProps) {
|
||||
const narrow = useNarrow();
|
||||
const { channelsDispatch } = useMessengerContext();
|
||||
|
||||
return (
|
||||
<ChannelWrapper
|
||||
className={`${isActive && "active"}`}
|
||||
isNarrow={narrow && activeView}
|
||||
onClick={onClick}
|
||||
id={!activeView ? `${channel.id + "contextMenu"}` : ""}
|
||||
>
|
||||
<ChannelInfo activeView={activeView}>
|
||||
<ChannelIcon channel={channel} activeView={activeView} />
|
||||
<ChannelTextInfo activeView={activeView && !narrow}>
|
||||
<ChannelNameWrapper>
|
||||
<ChannelName
|
||||
channel={channel}
|
||||
active={isActive || activeView || narrow}
|
||||
activeView={activeView}
|
||||
muted={channel?.isMuted}
|
||||
notified={notified}
|
||||
/>
|
||||
{channel?.isMuted && activeView && !narrow && (
|
||||
<MutedBtn
|
||||
onClick={() =>
|
||||
channelsDispatch({ type: "ToggleMuted", payload: channel.id })
|
||||
}
|
||||
>
|
||||
<MutedIcon />
|
||||
<Tooltip tip="Unmute" className="muted" />
|
||||
</MutedBtn>
|
||||
)}
|
||||
</ChannelNameWrapper>
|
||||
{activeView && (
|
||||
<ChannelDescription>{channel.description}</ChannelDescription>
|
||||
)}
|
||||
</ChannelTextInfo>
|
||||
</ChannelInfo>
|
||||
{!activeView && !!mention && !channel?.isMuted && (
|
||||
<NotificationBagde>{mention}</NotificationBagde>
|
||||
)}
|
||||
{channel?.isMuted && !activeView && <MutedIcon />}
|
||||
{!activeView && (
|
||||
<ChannelMenu
|
||||
channel={channel}
|
||||
setEditGroup={setEditGroup}
|
||||
className={narrow ? "sideNarrow" : "side"}
|
||||
/>
|
||||
)}
|
||||
</ChannelWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const ChannelWrapper = styled.div<{ isNarrow?: boolean }>`
|
||||
width: ${({ isNarrow }) => (isNarrow ? "calc(100% - 162px)" : "100%")};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
background-color: ${({ theme }) => theme.activeChannelBackground};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme, isNarrow }) => isNarrow && theme.border};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChannelInfo = styled.div<{ activeView?: boolean }>`
|
||||
display: flex;
|
||||
align-items: ${({ activeView }) => (activeView ? "flex-start" : "center")};
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const ChannelTextInfo = styled.div<{ activeView?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
padding: ${({ activeView }) => activeView && "0 24px 24px 0"};
|
||||
`;
|
||||
|
||||
const ChannelNameWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ChannelName = styled(RenderChannelName)<{
|
||||
muted?: boolean;
|
||||
notified?: boolean;
|
||||
active?: boolean;
|
||||
activeView?: boolean;
|
||||
}>`
|
||||
font-weight: ${({ notified, muted, active }) =>
|
||||
notified && !muted && !active ? "600" : "500"};
|
||||
opacity: ${({ notified, muted, active }) =>
|
||||
muted ? "0.4" : notified || active ? "1.0" : "0.7"};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
margin-right: ${({ muted, activeView }) =>
|
||||
muted && activeView ? "8px" : ""};
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
const ChannelDescription = styled.p`
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const NotificationBagde = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ theme }) => theme.notificationColor};
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const MutedBtn = styled.button`
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
|
||||
&:hover > svg {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { ChannelData } from "../../models/ChannelData";
|
||||
|
||||
interface ChannelIconProps {
|
||||
channel: ChannelData;
|
||||
activeView?: boolean;
|
||||
}
|
||||
|
||||
export function ChannelIcon({ channel, activeView }: ChannelIconProps) {
|
||||
const narrow = useNarrow();
|
||||
|
||||
return (
|
||||
<ChannelLogo
|
||||
icon={channel.icon}
|
||||
className={activeView ? "active" : narrow ? "narrow" : ""}
|
||||
>
|
||||
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
|
||||
</ChannelLogo>
|
||||
);
|
||||
}
|
||||
|
||||
export const ChannelLogo = styled.div<{ icon?: string }>`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
background-color: ${({ theme }) => theme.iconColor};
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-image: ${({ icon }) => icon && `url(${icon}`};
|
||||
color: ${({ theme }) => theme.iconTextColor};
|
||||
|
||||
&.active {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,164 @@
|
|||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { ChatState, useChatState } from "../../contexts/chatStateProvider";
|
||||
import { useIdentity } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { CreateIcon } from "../Icons/CreateIcon";
|
||||
import { UserCreation } from "../UserCreation/UserCreation";
|
||||
|
||||
import { Channel } from "./Channel";
|
||||
|
||||
interface ChannelsProps {
|
||||
onCommunityClick?: () => void;
|
||||
setEditGroup?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
type GenerateChannelsProps = ChannelsProps & {
|
||||
type: string;
|
||||
};
|
||||
|
||||
function GenerateChannels({
|
||||
type,
|
||||
onCommunityClick,
|
||||
setEditGroup,
|
||||
}: GenerateChannelsProps) {
|
||||
const { mentions, notifications, activeChannel, channelsDispatch, channels } =
|
||||
useMessengerContext();
|
||||
|
||||
const channelList = useMemo(() => Object.values(channels), [channels]);
|
||||
|
||||
const setChatState = useChatState()[1];
|
||||
return (
|
||||
<>
|
||||
{channelList
|
||||
.filter((channel) => channel.type === type)
|
||||
.map((channel) => (
|
||||
<Channel
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
isActive={channel.id === activeChannel?.id}
|
||||
notified={notifications?.[channel.id] > 0}
|
||||
mention={mentions?.[channel.id]}
|
||||
onClick={() => {
|
||||
channelsDispatch({ type: "ChangeActive", payload: channel.id });
|
||||
if (onCommunityClick) {
|
||||
onCommunityClick();
|
||||
}
|
||||
setChatState(ChatState.ChatBody);
|
||||
}}
|
||||
setEditGroup={setEditGroup}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type ChatsListProps = {
|
||||
onCommunityClick?: () => void;
|
||||
setEditGroup?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
function ChatsSideBar({ onCommunityClick, setEditGroup }: ChatsListProps) {
|
||||
const setChatState = useChatState()[1];
|
||||
return (
|
||||
<>
|
||||
<ChatsBar>
|
||||
<Heading>Messages</Heading>
|
||||
<EditBtn onClick={() => setChatState(ChatState.ChatCreation)}>
|
||||
<CreateIcon />
|
||||
</EditBtn>
|
||||
</ChatsBar>
|
||||
<ChatsList>
|
||||
<GenerateChannels
|
||||
type={"group"}
|
||||
onCommunityClick={onCommunityClick}
|
||||
setEditGroup={setEditGroup}
|
||||
/>
|
||||
<GenerateChannels type={"dm"} onCommunityClick={onCommunityClick} />
|
||||
</ChatsList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Channels({ onCommunityClick, setEditGroup }: ChannelsProps) {
|
||||
const identity = useIdentity();
|
||||
return (
|
||||
<ChannelList>
|
||||
<GenerateChannels type={"channel"} onCommunityClick={onCommunityClick} />
|
||||
<Chats>
|
||||
{identity ? (
|
||||
<ChatsSideBar
|
||||
onCommunityClick={onCommunityClick}
|
||||
setEditGroup={setEditGroup}
|
||||
/>
|
||||
) : (
|
||||
<UserCreation permission={true} />
|
||||
)}
|
||||
</Chats>
|
||||
</ChannelList>
|
||||
);
|
||||
}
|
||||
|
||||
export const ChannelList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Chats = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 24px);
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.primary};
|
||||
opacity: 0.1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatsBar = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const ChatsList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Heading = styled.p`
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
`;
|
||||
|
||||
const EditBtn = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${({ theme }) => theme.sectionBackgroundColor};
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,129 @@
|
|||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useUserPublicKey } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { ChannelData } from "../../models/ChannelData";
|
||||
import { textMediumStyles } from "../Text";
|
||||
|
||||
import { ChannelInfo, ChannelName } from "./Channel";
|
||||
import { ChannelLogo } from "./ChannelIcon";
|
||||
|
||||
type ChannelBeggingTextProps = {
|
||||
channel: ChannelData;
|
||||
};
|
||||
|
||||
function ChannelBeggingText({ channel }: ChannelBeggingTextProps) {
|
||||
const userPK = useUserPublicKey();
|
||||
const { contacts } = useMessengerContext();
|
||||
const members = useMemo(() => {
|
||||
if (channel?.members && userPK) {
|
||||
return channel.members
|
||||
.filter((contact) => contact.id !== userPK)
|
||||
.map((member) => contacts?.[member.id] ?? member);
|
||||
}
|
||||
return [];
|
||||
}, [channel, contacts, userPK]);
|
||||
|
||||
switch (channel.type) {
|
||||
case "dm":
|
||||
return (
|
||||
<EmptyText>
|
||||
Any messages you send here are encrypted and can only be read by you
|
||||
and <br />
|
||||
<span>{channel.name.slice(0, 10)}</span>.
|
||||
</EmptyText>
|
||||
);
|
||||
case "group":
|
||||
return (
|
||||
<EmptyTextGroup>
|
||||
{userPK && <span>{userPK}</span>} created a group with{" "}
|
||||
{members.map((contact, idx) => (
|
||||
<span key={contact.id}>
|
||||
{contact?.customName ?? contact.trueName.slice(0, 10)}
|
||||
{idx < members.length - 1 && <> and </>}
|
||||
</span>
|
||||
))}
|
||||
</EmptyTextGroup>
|
||||
);
|
||||
case "channel":
|
||||
return (
|
||||
<EmptyText>
|
||||
Welcome to the beginning of the <span>#{channel.name}</span> channel!
|
||||
</EmptyText>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type EmptyChannelProps = {
|
||||
channel: ChannelData;
|
||||
};
|
||||
|
||||
export function EmptyChannel({ channel }: EmptyChannelProps) {
|
||||
const narrow = useNarrow();
|
||||
|
||||
return (
|
||||
<Wrapper className={`${!narrow && "wide"}`}>
|
||||
<ChannelInfoEmpty>
|
||||
<ChannelLogoEmpty icon={channel.icon}>
|
||||
{" "}
|
||||
{!channel.icon && channel.name.slice(0, 1).toUpperCase()}
|
||||
</ChannelLogoEmpty>
|
||||
<ChannelNameEmpty active={true} channel={channel} />
|
||||
</ChannelInfoEmpty>
|
||||
<ChannelBeggingText channel={channel} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
&.wide {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChannelInfoEmpty = styled(ChannelInfo)`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const ChannelLogoEmpty = styled(ChannelLogo)`
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
font-weight: bold;
|
||||
font-size: 51px;
|
||||
line-height: 62px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const ChannelNameEmpty = styled(ChannelName)`
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const EmptyText = styled.p`
|
||||
display: inline-block;
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
max-width: 310px;
|
||||
text-align: center;
|
||||
|
||||
& > span {
|
||||
color: ${({ theme }) => theme.primary};
|
||||
}
|
||||
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
const EmptyTextGroup = styled(EmptyText)`
|
||||
& > span {
|
||||
word-break: break-all;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,182 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { Reply } from "../../hooks/useReply";
|
||||
import { ChannelData } from "../../models/ChannelData";
|
||||
import { TokenRequirement } from "../Form/TokenRequirement";
|
||||
import { MessagesList } from "../Messages/MessagesList";
|
||||
import { NarrowChannels } from "../NarrowMode/NarrowChannels";
|
||||
import { NarrowMembers } from "../NarrowMode/NarrowMembers";
|
||||
import { LoadingSkeleton } from "../Skeleton/LoadingSkeleton";
|
||||
|
||||
import { ChatCreation } from "./ChatCreation";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { ChatTopbar, ChatTopbarLoading } from "./ChatTopbar";
|
||||
|
||||
export enum ChatBodyState {
|
||||
Chat,
|
||||
Channels,
|
||||
Members,
|
||||
}
|
||||
|
||||
function ChatBodyLoading() {
|
||||
const narrow = useNarrow();
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatBodyWrapper className={narrow ? "narrow" : ""}>
|
||||
<ChatTopbarLoading />
|
||||
<LoadingSkeleton />
|
||||
<ChatInput reply={undefined} setReply={() => undefined} />
|
||||
</ChatBodyWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
type ChatBodyContentProps = {
|
||||
showState: ChatBodyState;
|
||||
switchShowState: (state: ChatBodyState) => void;
|
||||
channel: ChannelData;
|
||||
};
|
||||
|
||||
function ChatBodyContent({
|
||||
showState,
|
||||
switchShowState,
|
||||
channel,
|
||||
}: ChatBodyContentProps) {
|
||||
const [reply, setReply] = useState<Reply | undefined>(undefined);
|
||||
|
||||
switch (showState) {
|
||||
case ChatBodyState.Chat:
|
||||
return (
|
||||
<>
|
||||
<MessagesList setReply={setReply} channel={channel} />
|
||||
<ChatInput reply={reply} setReply={setReply} />
|
||||
</>
|
||||
);
|
||||
case ChatBodyState.Channels:
|
||||
return (
|
||||
<NarrowChannels
|
||||
setShowChannels={() => switchShowState(ChatBodyState.Channels)}
|
||||
/>
|
||||
);
|
||||
case ChatBodyState.Members:
|
||||
return (
|
||||
<NarrowMembers
|
||||
switchShowMembersList={() => switchShowState(ChatBodyState.Members)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatBodyProps {
|
||||
onClick: () => void;
|
||||
showMembers: boolean;
|
||||
permission: boolean;
|
||||
editGroup: boolean;
|
||||
setEditGroup: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export function ChatBody({
|
||||
onClick,
|
||||
showMembers,
|
||||
permission,
|
||||
editGroup,
|
||||
setEditGroup,
|
||||
}: ChatBodyProps) {
|
||||
const { activeChannel, loadingMessenger } = useMessengerContext();
|
||||
|
||||
const narrow = useNarrow();
|
||||
const className = useMemo(() => (narrow ? "narrow" : ""), [narrow]);
|
||||
|
||||
const [showState, setShowState] = useState<ChatBodyState>(ChatBodyState.Chat);
|
||||
const switchShowState = useCallback(
|
||||
(state: ChatBodyState) => {
|
||||
if (narrow) {
|
||||
setShowState((prev) => (prev === state ? ChatBodyState.Chat : state));
|
||||
}
|
||||
},
|
||||
[narrow]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!narrow) {
|
||||
setShowState(ChatBodyState.Chat);
|
||||
}
|
||||
}, [narrow]);
|
||||
|
||||
if (!loadingMessenger && activeChannel) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatBodyWrapper className={className}>
|
||||
{editGroup ? (
|
||||
<ChatCreation
|
||||
setEditGroup={setEditGroup}
|
||||
activeChannel={activeChannel}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ChatTopbar
|
||||
onClick={onClick}
|
||||
setEditGroup={setEditGroup}
|
||||
showMembers={showMembers}
|
||||
showState={showState}
|
||||
switchShowState={switchShowState}
|
||||
/>
|
||||
<ChatBodyContent
|
||||
showState={showState}
|
||||
switchShowState={switchShowState}
|
||||
channel={activeChannel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ChatBodyWrapper>
|
||||
{!permission && (
|
||||
<BluredWrapper>
|
||||
<TokenRequirement />
|
||||
</BluredWrapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return <ChatBodyLoading />;
|
||||
}
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
width: 61%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
position: relative;
|
||||
|
||||
&.narrow {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatBodyWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
`;
|
||||
|
||||
const BluredWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: ${({ theme }) => theme.bodyBackgroundGradient};
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 2;
|
||||
`;
|
|
@ -0,0 +1,387 @@
|
|||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { ChatState, useChatState } from "../../contexts/chatStateProvider";
|
||||
import { useUserPublicKey } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { ChannelData } from "../../models/ChannelData";
|
||||
import { ActivityButton } from "../ActivityCenter/ActivityButton";
|
||||
import { BackButton } from "../Buttons/BackButton";
|
||||
import { buttonStyles } from "../Buttons/buttonStyle";
|
||||
import { CrossIcon } from "../Icons/CrossIcon";
|
||||
import { Member } from "../Members/Member";
|
||||
import { SearchBlock } from "../SearchBlock";
|
||||
import { textMediumStyles } from "../Text";
|
||||
|
||||
import { ChatInput } from "./ChatInput";
|
||||
|
||||
interface ChatCreationProps {
|
||||
setEditGroup?: (val: boolean) => void;
|
||||
activeChannel?: ChannelData;
|
||||
}
|
||||
|
||||
export function ChatCreation({
|
||||
setEditGroup,
|
||||
activeChannel,
|
||||
}: ChatCreationProps) {
|
||||
const narrow = useNarrow();
|
||||
const userPK = useUserPublicKey();
|
||||
const [query, setQuery] = useState("");
|
||||
const [groupChatMembersIds, setGroupChatMembersIds] = useState<string[]>(
|
||||
activeChannel?.members?.map((member) => member.id) ?? []
|
||||
);
|
||||
const { contacts, createGroupChat, addMembers } = useMessengerContext();
|
||||
|
||||
const groupChatMembers = useMemo(
|
||||
() => groupChatMembersIds.map((id) => contacts[id]).filter((e) => !!e),
|
||||
[groupChatMembersIds, contacts]
|
||||
);
|
||||
|
||||
const contactsList = useMemo(() => {
|
||||
return Object.values(contacts)
|
||||
.filter(
|
||||
(member) =>
|
||||
member.id.includes(query) ||
|
||||
member?.customName?.includes(query) ||
|
||||
member.trueName.includes(query)
|
||||
)
|
||||
.filter((member) => !groupChatMembersIds.includes(member.id));
|
||||
}, [query, groupChatMembersIds, contacts]);
|
||||
|
||||
const setChatState = useChatState()[1];
|
||||
|
||||
const addMember = useCallback(
|
||||
(member: string) => {
|
||||
setGroupChatMembersIds((prevMembers: string[]) => {
|
||||
if (
|
||||
prevMembers.find((mem) => mem === member) ||
|
||||
prevMembers.length >= 5
|
||||
) {
|
||||
return prevMembers;
|
||||
} else {
|
||||
return [...prevMembers, member];
|
||||
}
|
||||
});
|
||||
setQuery("");
|
||||
},
|
||||
[setGroupChatMembersIds]
|
||||
);
|
||||
const removeMember = useCallback(
|
||||
(member: string) => {
|
||||
setGroupChatMembersIds((prev) => prev.filter((e) => e != member));
|
||||
},
|
||||
[setGroupChatMembersIds]
|
||||
);
|
||||
|
||||
const createChat = useCallback(
|
||||
(group: string[]) => {
|
||||
if (userPK) {
|
||||
const newGroup = group.slice();
|
||||
newGroup.push(userPK);
|
||||
createGroupChat(newGroup);
|
||||
setChatState(ChatState.ChatBody);
|
||||
}
|
||||
},
|
||||
[userPK, createGroupChat, setChatState]
|
||||
);
|
||||
|
||||
const handleCreationClick = useCallback(() => {
|
||||
if (!activeChannel) {
|
||||
createChat(groupChatMembers.map((member) => member.id));
|
||||
} else {
|
||||
addMembers(
|
||||
groupChatMembers.map((member) => member.id),
|
||||
activeChannel.id
|
||||
);
|
||||
}
|
||||
setEditGroup?.(false);
|
||||
}, [activeChannel, groupChatMembers, createChat, addMembers, setEditGroup]);
|
||||
|
||||
return (
|
||||
<CreationWrapper className={`${narrow && "narrow"}`}>
|
||||
<CreationBar
|
||||
className={`${groupChatMembers.length === 5 && narrow && "limit"}`}
|
||||
>
|
||||
{narrow && (
|
||||
<BackButton
|
||||
onBtnClick={() =>
|
||||
setEditGroup
|
||||
? setEditGroup?.(false)
|
||||
: setChatState(ChatState.ChatBody)
|
||||
}
|
||||
className="narrow"
|
||||
/>
|
||||
)}
|
||||
<Column>
|
||||
<InputBar>
|
||||
<InputText>To:</InputText>
|
||||
<StyledList>
|
||||
{groupChatMembers.map((member) => (
|
||||
<StyledMember key={member.id}>
|
||||
<StyledName>
|
||||
{member?.customName?.slice(0, 10) ??
|
||||
member.trueName.slice(0, 10)}
|
||||
</StyledName>
|
||||
<CloseButton onClick={() => removeMember(member.id)}>
|
||||
<CrossIcon memberView={true} />
|
||||
</CloseButton>
|
||||
</StyledMember>
|
||||
))}
|
||||
</StyledList>
|
||||
{groupChatMembers.length < 5 && (
|
||||
<SearchMembers>
|
||||
<Input
|
||||
value={query}
|
||||
onInput={(e) => setQuery(e.currentTarget.value)}
|
||||
/>
|
||||
</SearchMembers>
|
||||
)}
|
||||
{!narrow && groupChatMembers.length === 5 && (
|
||||
<LimitAlert>5 user Limit reached</LimitAlert>
|
||||
)}
|
||||
</InputBar>
|
||||
{narrow && groupChatMembers.length === 5 && (
|
||||
<LimitAlert className="narrow">5 user Limit reached</LimitAlert>
|
||||
)}
|
||||
</Column>
|
||||
<CreationBtn
|
||||
disabled={groupChatMembers.length === 0}
|
||||
onClick={handleCreationClick}
|
||||
>
|
||||
Confirm
|
||||
</CreationBtn>
|
||||
{!narrow && <ActivityButton className="creation" />}
|
||||
{!narrow && (
|
||||
<SearchBlock
|
||||
query={query}
|
||||
discludeList={groupChatMembersIds}
|
||||
onClick={addMember}
|
||||
/>
|
||||
)}
|
||||
</CreationBar>
|
||||
{((!setEditGroup && groupChatMembers.length === 0) || narrow) &&
|
||||
Object.keys(contacts).length > 0 && (
|
||||
<Contacts>
|
||||
<ContactsHeading>Contacts</ContactsHeading>
|
||||
<ContactsList>
|
||||
{userPK && narrow
|
||||
? contactsList.map((contact) => (
|
||||
<Contact key={contact.id}>
|
||||
<Member
|
||||
contact={contact}
|
||||
isOnline={contact.online}
|
||||
onClick={() => addMember(contact.id)}
|
||||
/>
|
||||
</Contact>
|
||||
))
|
||||
: Object.values(contacts)
|
||||
.filter(
|
||||
(e) =>
|
||||
e.id != userPK && !groupChatMembersIds.includes(e.id)
|
||||
)
|
||||
.map((contact) => (
|
||||
<Contact key={contact.id}>
|
||||
<Member
|
||||
contact={contact}
|
||||
isOnline={contact.online}
|
||||
onClick={() => addMember(contact.id)}
|
||||
/>
|
||||
</Contact>
|
||||
))}
|
||||
</ContactsList>
|
||||
</Contacts>
|
||||
)}
|
||||
{!setEditGroup && Object.keys(contacts).length === 0 && (
|
||||
<EmptyContacts>
|
||||
<EmptyContactsHeading>
|
||||
You only can send direct messages to your Contacts.{" "}
|
||||
</EmptyContactsHeading>
|
||||
<EmptyContactsHeading>
|
||||
{" "}
|
||||
Send a contact request to the person you would like to chat with,
|
||||
you will be able to chat with them once they have accepted your
|
||||
contact request.
|
||||
</EmptyContactsHeading>
|
||||
</EmptyContacts>
|
||||
)}
|
||||
|
||||
{!activeChannel && (
|
||||
<ChatInput
|
||||
createChat={createChat}
|
||||
group={groupChatMembers.map((member) => member.id)}
|
||||
/>
|
||||
)}
|
||||
</CreationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const CreationWrapper = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
background-color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
padding: 8px 16px;
|
||||
|
||||
&.narrow {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const CreationBar = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
|
||||
&.limit {
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
const Column = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const InputBar = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background-color: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
border-radius: 8px;
|
||||
padding: 6px 16px;
|
||||
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
width: 100%;
|
||||
min-width: 20px;
|
||||
background-color: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
outline: none;
|
||||
resize: none;
|
||||
|
||||
${textMediumStyles}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
caret-color: ${({ theme }) => theme.notificationColor};
|
||||
}
|
||||
`;
|
||||
|
||||
const InputText = styled.div`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const CreationBtn = styled.button`
|
||||
padding: 11px 24px;
|
||||
${buttonStyles}
|
||||
|
||||
&:disabled {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledList = styled.div`
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
margin-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMember = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: ${({ theme }) => theme.tertiary};
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
border-radius: 8px;
|
||||
|
||||
& + & {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledName = styled.p`
|
||||
color: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`;
|
||||
|
||||
const Contacts = styled.div`
|
||||
height: calc(100% - 44px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const Contact = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 12px 0 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactsHeading = styled.p`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
export const ContactsList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const EmptyContacts = styled(Contacts)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const EmptyContactsHeading = styled(ContactsHeading)`
|
||||
max-width: 550px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const SearchMembers = styled.div`
|
||||
position: relative;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const LimitAlert = styled.p`
|
||||
text-transform: uppercase;
|
||||
margin-left: auto;
|
||||
color: ${({ theme }) => theme.redColor};
|
||||
white-space: nowrap;
|
||||
|
||||
&.narrow {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,562 @@
|
|||
import { EmojiData } from "emoji-mart";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { ChatState, useChatState } from "../../contexts/chatStateProvider";
|
||||
import { useIdentity } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useModal } from "../../contexts/modalProvider";
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||
import { Reply } from "../../hooks/useReply";
|
||||
import { uintToImgUrl } from "../../utils/uintToImgUrl";
|
||||
import { ClearBtn } from "../Form/inputStyles";
|
||||
import { ClearSvg } from "../Icons/ClearIcon";
|
||||
import { ClearSvgFull } from "../Icons/ClearIconFull";
|
||||
import { EmojiIcon } from "../Icons/EmojiIcon";
|
||||
import { GifIcon } from "../Icons/GifIcon";
|
||||
import { PictureIcon } from "../Icons/PictureIcon";
|
||||
import { ReplySvg } from "../Icons/ReplyIcon";
|
||||
import { StickerIcon } from "../Icons/StickerIcon";
|
||||
import "emoji-mart/css/emoji-mart.css";
|
||||
import { SizeLimitModal, SizeLimitModalName } from "../Modals/SizeLimitModal";
|
||||
import { UserCreationStartModalName } from "../Modals/UserCreationStartModal";
|
||||
import { SearchBlock } from "../SearchBlock";
|
||||
import { textMediumStyles, textSmallStyles } from "../Text";
|
||||
|
||||
import { EmojiPicker } from "./EmojiPicker";
|
||||
|
||||
interface ChatInputProps {
|
||||
reply?: Reply | undefined;
|
||||
setReply?: (val: Reply | undefined) => void;
|
||||
createChat?: (group: string[]) => void;
|
||||
group?: string[];
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
reply,
|
||||
setReply,
|
||||
createChat,
|
||||
group,
|
||||
}: ChatInputProps) {
|
||||
const narrow = useNarrow();
|
||||
const identity = useIdentity();
|
||||
const setChatState = useChatState()[1];
|
||||
const disabled = useMemo(() => !identity, [identity]);
|
||||
const { sendMessage, contacts } = useMessengerContext();
|
||||
const [content, setContent] = useState("");
|
||||
const [clearComponent, setClearComponent] = useState("");
|
||||
const [showEmoji, setShowEmoji] = useState(false);
|
||||
const [inputHeight, setInputHeight] = useState(40);
|
||||
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined);
|
||||
|
||||
const { setModal } = useModal(SizeLimitModalName);
|
||||
const { setModal: setCreationStartModal } = useModal(
|
||||
UserCreationStartModalName
|
||||
);
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const ref = useRef(null);
|
||||
useClickOutside(ref, () => setShowEmoji(false));
|
||||
|
||||
const image = useMemo(
|
||||
() => (imageUint ? uintToImgUrl(imageUint) : ""),
|
||||
[imageUint]
|
||||
);
|
||||
|
||||
const addEmoji = useCallback(
|
||||
(e: EmojiData) => {
|
||||
if ("unified" in e) {
|
||||
const sym = e.unified.split("-");
|
||||
const codesArray: string[] = [];
|
||||
sym.forEach((el: string) => codesArray.push("0x" + el));
|
||||
const emoji = String.fromCodePoint(
|
||||
...(codesArray as unknown as number[])
|
||||
);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.appendChild(document.createTextNode(emoji));
|
||||
}
|
||||
setContent((p) => p + emoji);
|
||||
}
|
||||
},
|
||||
[setContent]
|
||||
);
|
||||
|
||||
const resizeTextArea = useCallback((target: HTMLDivElement) => {
|
||||
target.style.height = "40px";
|
||||
target.style.height = `${Math.min(target.scrollHeight, 438)}px`;
|
||||
setInputHeight(target.scrollHeight);
|
||||
}, []);
|
||||
|
||||
const rowHeight = inputHeight + (image ? 73 : 0);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLDivElement>) => {
|
||||
const element = document.getSelection();
|
||||
const inputElement = inputRef.current;
|
||||
if (inputElement && element && element.rangeCount > 0) {
|
||||
const selection = element?.getRangeAt(0)?.startOffset;
|
||||
const parentElement = element.anchorNode?.parentElement;
|
||||
if (parentElement && parentElement.tagName === "B") {
|
||||
parentElement.outerHTML = parentElement.innerText;
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
if (element.anchorNode.firstChild) {
|
||||
const childNumber =
|
||||
element.focusOffset === 0 ? 0 : element.focusOffset - 1;
|
||||
range.setStart(
|
||||
element.anchorNode.childNodes[childNumber],
|
||||
selection
|
||||
);
|
||||
}
|
||||
range.collapse(true);
|
||||
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
const target = e.target;
|
||||
resizeTextArea(target);
|
||||
setContent(target.textContent ?? "");
|
||||
},
|
||||
[resizeTextArea]
|
||||
);
|
||||
|
||||
const onInputKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key == "Enter" && !e.getModifierState("Shift")) {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLDivElement).style.height = "40px";
|
||||
setInputHeight(40);
|
||||
sendMessage(content, imageUint, reply?.id);
|
||||
setImageUint(undefined);
|
||||
setClearComponent("");
|
||||
if (inputRef.current) {
|
||||
inputRef.current.innerHTML = "";
|
||||
}
|
||||
setContent("");
|
||||
if (setReply) setReply(undefined);
|
||||
if (createChat && group) {
|
||||
createChat(group);
|
||||
setChatState(ChatState.ChatBody);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
content,
|
||||
imageUint,
|
||||
createChat,
|
||||
group,
|
||||
sendMessage,
|
||||
reply?.id,
|
||||
setChatState,
|
||||
setReply,
|
||||
]
|
||||
);
|
||||
|
||||
const [selectedElement, setSelectedElement] = useState<{
|
||||
element: Selection | null;
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
node: Node | null;
|
||||
}>({ element: null, start: 0, end: 0, text: "", node: null });
|
||||
|
||||
const handleCursorChange = useCallback(() => {
|
||||
const element = document.getSelection();
|
||||
if (element && element.rangeCount > 0) {
|
||||
const selection = element?.getRangeAt(0)?.startOffset;
|
||||
const text = element?.anchorNode?.textContent;
|
||||
if (selection && text) {
|
||||
const end = text.indexOf(" ", selection);
|
||||
const start = text.lastIndexOf(" ", selection - 1);
|
||||
setSelectedElement({
|
||||
element,
|
||||
start,
|
||||
end,
|
||||
text,
|
||||
node: element.anchorNode,
|
||||
});
|
||||
const substring = text.substring(
|
||||
start > -1 ? start + 1 : 0,
|
||||
end > -1 ? end : undefined
|
||||
);
|
||||
if (substring.startsWith("@")) {
|
||||
setQuery(substring.slice(1));
|
||||
} else {
|
||||
setQuery("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(handleCursorChange, [content, handleCursorChange]);
|
||||
|
||||
const addMention = useCallback(
|
||||
(contact: string) => {
|
||||
if (inputRef?.current) {
|
||||
const { element, start, end, text, node } = selectedElement;
|
||||
if (element && text && node) {
|
||||
const firstSlice = text.slice(0, start > -1 ? start : 0);
|
||||
const secondSlice = text.slice(end > -1 ? end : content.length);
|
||||
const replaceContent = `${firstSlice} @${contact}${secondSlice}`;
|
||||
const spaceElement = document.createTextNode(" ");
|
||||
const contactElement = document.createElement("span");
|
||||
contactElement.innerText = `@${contact}`;
|
||||
|
||||
if (contactElement && element.rangeCount > 0) {
|
||||
const range = element.getRangeAt(0);
|
||||
range.setStart(node, start > -1 ? start : 0);
|
||||
if (end === -1 || end > text.length) {
|
||||
range.setEnd(node, text.length);
|
||||
} else {
|
||||
range.setEnd(node, end);
|
||||
}
|
||||
range.deleteContents();
|
||||
if (end === -1) {
|
||||
range.insertNode(spaceElement.cloneNode());
|
||||
}
|
||||
range.insertNode(contactElement);
|
||||
if (start > -1) {
|
||||
range.insertNode(spaceElement.cloneNode());
|
||||
}
|
||||
range.collapse();
|
||||
}
|
||||
inputRef.current.focus();
|
||||
setQuery("");
|
||||
setContent(replaceContent);
|
||||
resizeTextArea(inputRef.current);
|
||||
}
|
||||
}
|
||||
},
|
||||
[inputRef, content, selectedElement, resizeTextArea]
|
||||
);
|
||||
|
||||
return (
|
||||
<View className={`${createChat && "creation"}`}>
|
||||
<SizeLimitModal />
|
||||
<AddPictureInputWrapper>
|
||||
<PictureIcon />
|
||||
<AddPictureInput
|
||||
disabled={disabled}
|
||||
type="file"
|
||||
multiple={true}
|
||||
accept="image/png, image/jpeg"
|
||||
onChange={(e) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = (s) => {
|
||||
const arr = new Uint8Array(s.target?.result as ArrayBuffer);
|
||||
setImageUint(arr);
|
||||
};
|
||||
|
||||
if (e?.target?.files?.[0]) {
|
||||
if (e.target.files[0].size < 1024 * 1024) {
|
||||
fileReader.readAsArrayBuffer(e.target.files[0]);
|
||||
} else {
|
||||
setModal(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AddPictureInputWrapper>
|
||||
<InputArea>
|
||||
{reply && (
|
||||
<ReplyWrapper>
|
||||
<ReplyTo>
|
||||
{" "}
|
||||
<ReplySvg width={18} height={18} className="input" />{" "}
|
||||
{contacts[reply.sender]?.customName ??
|
||||
contacts[reply.sender].trueName}
|
||||
</ReplyTo>
|
||||
<ReplyOn>{reply.content}</ReplyOn>
|
||||
{reply.image && <ImagePreview src={reply.image} />}
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
if (setReply) setReply(undefined);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<ClearSvg width={20} height={20} className="input" />
|
||||
</CloseButton>
|
||||
</ReplyWrapper>
|
||||
)}
|
||||
<Row style={{ height: `${rowHeight}px` }}>
|
||||
<InputWrapper>
|
||||
{image && (
|
||||
<ImageWrapper>
|
||||
<ImagePreview src={image} />
|
||||
<ClearImgBtn onClick={() => setImageUint(undefined)}>
|
||||
<ClearSvgFull width={20} height={20} />
|
||||
</ClearImgBtn>
|
||||
</ImageWrapper>
|
||||
)}
|
||||
{narrow && !identity ? (
|
||||
<JoinBtn onClick={() => setCreationStartModal(true)}>
|
||||
Click here to join discussion
|
||||
</JoinBtn>
|
||||
) : (
|
||||
<Input
|
||||
aria-disabled={disabled}
|
||||
contentEditable={!disabled}
|
||||
onInput={onInputChange}
|
||||
onKeyDown={onInputKeyPress}
|
||||
onKeyUp={handleCursorChange}
|
||||
ref={inputRef}
|
||||
onClick={handleCursorChange}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: disabled
|
||||
? "You need to join this community to send messages"
|
||||
: clearComponent,
|
||||
}}
|
||||
className={`${disabled && "disabled"} `}
|
||||
/>
|
||||
)}
|
||||
{query && (
|
||||
<SearchBlock
|
||||
query={query}
|
||||
discludeList={[]}
|
||||
onClick={addMention}
|
||||
onBotttom
|
||||
/>
|
||||
)}
|
||||
</InputWrapper>
|
||||
<InputButtons>
|
||||
<EmojiWrapper ref={ref}>
|
||||
<ChatButton
|
||||
onClick={() => {
|
||||
if (!disabled) setShowEmoji(!showEmoji);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EmojiIcon isActive={showEmoji} />
|
||||
</ChatButton>
|
||||
<EmojiPicker
|
||||
addEmoji={addEmoji}
|
||||
showEmoji={showEmoji}
|
||||
bottom={rowHeight - 24}
|
||||
/>
|
||||
</EmojiWrapper>
|
||||
<ChatButton disabled={disabled}>
|
||||
<StickerIcon />
|
||||
</ChatButton>
|
||||
<ChatButton disabled={disabled}>
|
||||
<GifIcon />
|
||||
</ChatButton>
|
||||
</InputButtons>
|
||||
</Row>
|
||||
</InputArea>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const EmojiWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const View = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 6px 8px 6px 10px;
|
||||
position: relative;
|
||||
|
||||
&.creation {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const InputArea = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 438px;
|
||||
padding: 2px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-height: 438px;
|
||||
padding-right: 6px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
`;
|
||||
|
||||
const InputButtons = styled.div`
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
|
||||
button + button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
width: 64px;
|
||||
position: relative;
|
||||
`;
|
||||
const ImagePreview = styled.img`
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
margin-left: 8px;
|
||||
margin-top: 9px;
|
||||
`;
|
||||
|
||||
const ClearImgBtn = styled(ClearBtn)`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 4px;
|
||||
right: -20px;
|
||||
transform: none;
|
||||
padding: 0;
|
||||
border: 2px solid ${({ theme }) => theme.inputColor};
|
||||
background-color: ${({ theme }) => theme.inputColor};
|
||||
`;
|
||||
|
||||
const Input = styled.div`
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
max-height: 438px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 8px 0 8px 12px;
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border: 1px solid ${({ theme }) => theme.inputColor};
|
||||
color: ${({ theme }) => theme.primary};
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
outline: none;
|
||||
|
||||
${textMediumStyles};
|
||||
|
||||
&.disabled {
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
caret-color: ${({ theme }) => theme.notificationColor};
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline;
|
||||
color: ${({ theme }) => theme.mentionColor};
|
||||
background: ${({ theme }) => theme.mentionBg};
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.mentionBgHover};
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AddPictureInputWrapper = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 4px;
|
||||
|
||||
& > input[type="file"]::-webkit-file-upload-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > input:disabled::-webkit-file-upload-button {
|
||||
cursor: default;
|
||||
}
|
||||
`;
|
||||
|
||||
const AddPictureInput = styled.input`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
const ChatButton = styled.button`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
`;
|
||||
|
||||
const CloseButton = styled(ChatButton)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
`;
|
||||
|
||||
const ReplyWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: ${({ theme }) => theme.primary};
|
||||
border-radius: 14px 14px 4px 14px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ReplyTo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
${textSmallStyles};
|
||||
`;
|
||||
|
||||
export const ReplyOn = styled.div`
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
${textSmallStyles};
|
||||
`;
|
||||
|
||||
const JoinBtn = styled.button`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
background: ${({ theme }) => theme.inputColor};
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0 10px;
|
||||
text-align: start;
|
||||
|
||||
${textMediumStyles};
|
||||
`;
|
|
@ -0,0 +1,233 @@
|
|||
import { decode } from "html-entities";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useFetchMetadata } from "../../contexts/fetchMetadataProvider";
|
||||
import { useUserPublicKey } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||
import { ChatMessage } from "../../models/ChatMessage";
|
||||
import { Metadata } from "../../models/Metadata";
|
||||
import { ContactMenu } from "../Form/ContactMenu";
|
||||
import { ImageMenu } from "../Form/ImageMenu";
|
||||
import { textMediumStyles, textSmallStyles } from "../Text";
|
||||
|
||||
interface MentionProps {
|
||||
id: string;
|
||||
setMentioned: (val: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Mention({ id, setMentioned, className }: MentionProps) {
|
||||
const { contacts } = useMessengerContext();
|
||||
const contact = useMemo(() => contacts[id.slice(1)], [id, contacts]);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const userPK = useUserPublicKey();
|
||||
|
||||
useEffect(() => {
|
||||
if (userPK && contact) {
|
||||
if (contact.id === userPK) setMentioned(true);
|
||||
}
|
||||
}, [contact, userPK, setMentioned]);
|
||||
|
||||
const ref = useRef(null);
|
||||
useClickOutside(ref, () => setShowMenu(false));
|
||||
|
||||
if (!contact) return <>{id}</>;
|
||||
return (
|
||||
<MentionBLock
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className={className}
|
||||
ref={ref}
|
||||
>
|
||||
{`@${contact?.customName ?? contact.trueName}`}
|
||||
{showMenu && <ContactMenu id={id.slice(1)} setShowMenu={setShowMenu} />}
|
||||
</MentionBLock>
|
||||
);
|
||||
}
|
||||
|
||||
type ChatMessageContentProps = {
|
||||
message: ChatMessage;
|
||||
setImage: (image: string) => void;
|
||||
setLinkOpen: (link: string) => void;
|
||||
setMentioned: (val: boolean) => void;
|
||||
};
|
||||
|
||||
export function ChatMessageContent({
|
||||
message,
|
||||
setImage,
|
||||
setLinkOpen,
|
||||
setMentioned,
|
||||
}: ChatMessageContentProps) {
|
||||
const fetchMetadata = useFetchMetadata();
|
||||
const { content, image } = useMemo(() => message, [message]);
|
||||
const [elements, setElements] = useState<(string | React.ReactElement)[]>([
|
||||
content,
|
||||
]);
|
||||
const [link, setLink] = useState<string | undefined>(undefined);
|
||||
const [openGraph, setOpenGraph] = useState<Metadata | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
let link;
|
||||
const split = content.split(" ");
|
||||
const newSplit = split.flatMap((element, idx) => {
|
||||
if (element.startsWith("http://") || element.startsWith("https://")) {
|
||||
link = element;
|
||||
return [
|
||||
<Link key={idx} onClick={() => setLinkOpen(element)}>
|
||||
{element}
|
||||
</Link>,
|
||||
" ",
|
||||
];
|
||||
}
|
||||
if (element.startsWith("@")) {
|
||||
return [
|
||||
<Mention key={idx} id={element} setMentioned={setMentioned} />,
|
||||
" ",
|
||||
];
|
||||
}
|
||||
return [element, " "];
|
||||
});
|
||||
newSplit.pop();
|
||||
setLink(link);
|
||||
setElements(newSplit);
|
||||
}, [content, setLink, setMentioned, setElements, setLinkOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const updatePreview = async () => {
|
||||
if (link && fetchMetadata) {
|
||||
try {
|
||||
const metadata = await fetchMetadata(link);
|
||||
if (metadata) {
|
||||
setOpenGraph(metadata);
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
updatePreview();
|
||||
}, [link, fetchMetadata]);
|
||||
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<div>{elements.map((el) => el)}</div>
|
||||
{image && (
|
||||
<MessageImageWrapper>
|
||||
<MessageImage
|
||||
src={image}
|
||||
id={image}
|
||||
onClick={() => {
|
||||
setImage(image);
|
||||
}}
|
||||
/>
|
||||
<ImageMenu imageId={image} />
|
||||
</MessageImageWrapper>
|
||||
)}
|
||||
{openGraph && (
|
||||
<PreviewWrapper onClick={() => setLinkOpen(link ?? "")}>
|
||||
<PreviewImage src={decodeURI(decode(openGraph["og:image"]))} />
|
||||
<PreviewTitleWrapper>{openGraph["og:title"]}</PreviewTitleWrapper>
|
||||
<PreviewSiteNameWrapper>
|
||||
{openGraph["og:site_name"]}
|
||||
</PreviewSiteNameWrapper>
|
||||
</PreviewWrapper>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const MessageImageWrapper = styled.div`
|
||||
width: 147px;
|
||||
height: 196px;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const MessageImage = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const PreviewSiteNameWrapper = styled.div`
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
margin-top: 2px;
|
||||
color: #939ba1;
|
||||
margin-left: 12px;
|
||||
`;
|
||||
|
||||
const PreviewTitleWrapper = styled.div`
|
||||
margin-top: 7px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
width: 290px;
|
||||
margin-left: 12px;
|
||||
|
||||
${textSmallStyles}
|
||||
`;
|
||||
|
||||
const PreviewImage = styled.img`
|
||||
border-radius: 15px 15px 15px 4px;
|
||||
width: 305px;
|
||||
height: 170px;
|
||||
`;
|
||||
|
||||
const PreviewWrapper = styled.div`
|
||||
margin-top: 9px;
|
||||
background: #ffffff;
|
||||
width: 305px;
|
||||
height: 224px;
|
||||
border: 1px solid #eef2f5;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const MentionBLock = styled.div`
|
||||
display: inline-flex;
|
||||
color: ${({ theme }) => theme.mentionColor};
|
||||
background: ${({ theme }) => theme.mentionBgHover};
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.mentionHover};
|
||||
}
|
||||
|
||||
&.activity {
|
||||
max-width: 488px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
${textMediumStyles}
|
||||
`;
|
||||
|
||||
const Link = styled.a`
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.memberNameColor};
|
||||
`;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue