Move to ESM, Vite, Vitest and Turborepo (#286)

* fix example hot module replacement

* add turbo

* migrate to vite

* use turbo for running scripts

* migrate testing to vitest

* set yarn in settings.json

* set noEmit in base tsconfig

* update yarn.lock

* move protos to src

* remove relative paths from status-js

* remove unused files

* update declaration dir

* use vite-node as a debugging runtime

* fix test

* unify tests

* fix test case typo
This commit is contained in:
Pavel 2022-06-28 16:40:39 +02:00 committed by GitHub
parent 36f448cb96
commit 680ce2f79b
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
69 changed files with 1336 additions and 4466 deletions

3
.gitignore vendored
View File

@ -72,3 +72,6 @@ node_modules/
# Serverless directories # Serverless directories
.serverless/ .serverless/
# Turborepo
.turbo

View File

@ -4,6 +4,7 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"zxh404.vscode-proto3", "zxh404.vscode-proto3",
"ms-vscode.hexeditor", "ms-vscode.hexeditor",
"Orta.vscode-jest" "zixuanchen.vitest-explorer",
"mikestead.dotenv"
] ]
} }

33
.vscode/launch.json vendored
View File

@ -4,20 +4,27 @@
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Client", "name": "Debug Client",
"program": "${workspaceFolder}/packages/status-js/src/index.ts", "autoAttachChildProcesses": true,
"preLaunchTask": "npm: build:status-js", "skipFiles": ["<node_internals>/**"],
"outFiles": ["${workspaceFolder}/packages/status-js/dist/index.js"], "cwd": "${workspaceFolder}/packages/status-js",
"sourceMaps": true, "program": "${workspaceRoot}/node_modules/vite-node/dist/cli.mjs",
"args": ["src/index.ts"],
"smartStep": true, "smartStep": true,
"internalConsoleOptions": "openOnSessionStart", "console": "integratedTerminal",
"skipFiles": [ "sourceMaps": true
"${workspaceFolder}/node_modules/**/*", },
"<node_internals>/**/*.js" {
], "type": "node",
"sourceMapPathOverrides": { "request": "launch",
"packages/status-js/*": "${workspaceFolder}/packages/status-js/*" "name": "Debug Test File",
} "autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal",
"sourceMaps": true
} }
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"jestRunIt.debugTestLabel": "Debug", "eslint.packageManager": "yarn",
"jestRunIt.runTestLabel": "Run" "npm.packageManager": "yarn"
} }

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="icon" type="image/png" href="public/favicon.png" /> <link rel="icon" type="image/png" href="./public/favicon.png" />
<link <link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
@ -19,6 +19,6 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./index.tsx"></script> <script type="module" src="./src/index.tsx"></script>
</body> </body>
</html> </html>

View File

@ -1,12 +1,12 @@
{ {
"name": "community", "name": "community",
"type": "module",
"version": "0.0.0", "version": "0.0.0",
"source": "./index.html",
"browserslist": "> 0.5%, last 2 versions, not dead, not ios_saf < 13", "browserslist": "> 0.5%, last 2 versions, not dead, not ios_saf < 13",
"scripts": { "scripts": {
"dev": "parcel --https", "dev": "parcel index.html --https --no-cache --open",
"prebuild": "rm -rf dist", "prebuild": "rm -rf dist",
"build": "parcel build" "build": "parcel build index.html"
}, },
"dependencies": { "dependencies": {
"@status-im/react": "^0.0.0", "@status-im/react": "^0.0.0",
@ -17,10 +17,7 @@
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"parcel": "^2.0.0", "parcel": "^2.0.0",
"assert": "^2.0.0", "typescript": "^4.0.0",
"crypto-browserify": "^3.12.0", "process": "^0.11.10"
"process": "^0.11.10",
"stream-browserify": "^3.0.0",
"typescript": "^4.0.0"
} }
} }

View File

@ -0,0 +1,7 @@
import React from 'react'
import { Community } from '@status-im/react'
export const App = () => {
return <Community publicKey="<YOUR_COMMUNITY_KEY>" theme="light" />
}

View File

@ -1,16 +1,7 @@
import React, { StrictMode } from 'react' import React, { StrictMode } from 'react'
import { render } from 'react-dom' import { render } from 'react-dom'
import { Community } from '@status-im/react' import { App } from './app'
const App = () => {
return (
<Community
publicKey="<YOUR_COMMUNITY_KEY>"
theme="light"
/>
)
}
render( render(
<StrictMode> <StrictMode>

View File

@ -2,4 +2,5 @@ body,
html, html,
#root { #root {
height: 100%; height: 100%;
overscroll-behavior-y: none;
} }

View File

@ -1,16 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
// preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
preset: 'ts-jest/presets/default-esm',
globals: {
'ts-jest': {
useESM: true,
tsconfig: 'tsconfig.base.json',
},
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
}

View File

@ -1,8 +1,5 @@
{ {
"alias": { "type": "module",
"protons-runtime": "./node_modules/protons-runtime/dist/src/index.js",
"uint8arraylist": "./node_modules/uint8arraylist/dist/src/index.js"
},
"private": true, "private": true,
"workspaces": [ "workspaces": [
"packages/*", "packages/*",
@ -11,30 +8,19 @@
"keywords": [], "keywords": [],
"scripts": { "scripts": {
"prepare": "husky install", "prepare": "husky install",
"fix": "run-s 'fix:*' && wsrun -e -c -s fix", "test": "turbo run test --filter=@status-im/* -- --run",
"build": "wsrun -e -c -s build", "dev": "turbo run dev --parallel --filter=@status-im/*",
"build:status-js": "yarn workspace @status-im/js build", "build": "turbo run build --filter=@status-im/*",
"build:status-react": "yarn workspace @status-im/react build", "lint": "turbo run lint --filter=@status-im/*",
"lint": "eslint 'packages/**/*.{ts,tsx}'", "check": "turbo run check --filter=@status-im/*",
"lint:fix": "eslint 'packages/**/*.{ts,tsx}' --fix",
"lint:examples": "eslint 'examples/**/*.{ts,tsx}'",
"format": "prettier --write .", "format": "prettier --write .",
"typecheck": "wsrun -e -c -s typecheck", "format:check": "prettier --check .",
"test": "jest" "clean": "turbo run clean && rm -rf node_modules .parcel-cache"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.2", "@tsconfig/strictest": "^1.0.1",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@parcel/config-default": "^2.6.0",
"@parcel/packager-ts": "^2.6.0",
"@parcel/transformer-js": "^2.6.0",
"@parcel/transformer-react-refresh-wrap": "^2.6.0",
"@parcel/transformer-typescript-types": "^2.6.0",
"@types/jest": "^27.5.1",
"@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0", "@typescript-eslint/parser": "^5.12.0",
"babel-jest": "^28.1.0",
"eslint": "^8.9.0", "eslint": "^8.9.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-node": "^0.3.6",
@ -48,14 +34,13 @@
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-simple-import-sort": "^7.0.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"jest": "^28.1.0",
"lint-staged": "^12.3.4", "lint-staged": "^12.3.4",
"npm-run-all": "^4.1.5", "prettier": "^2.7.1",
"parcel": "^2.6.0", "turbo": "^1.3.1",
"prettier": "^2.5.1",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"wsrun": "^5.2.4", "vite": "^2.9.12",
"ts-jest": "^28.0.4" "vite-node": "^0.16.0",
"vitest": "^0.16.0"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,js,jsx}": [ "*.{ts,tsx,js,jsx}": [
@ -66,5 +51,9 @@
"prettier --write" "prettier --write"
] ]
}, },
"packageManager": "yarn@1.22.17" "packageManager": "yarn@1.22.17",
"alias": {
"protons-runtime": "./node_modules/protons-runtime/dist/src/index.js",
"uint8arraylist": "./node_modules/uint8arraylist/dist/src/index.js"
}
} }

View File

@ -2,6 +2,14 @@
"name": "@status-im/js", "name": "@status-im/js",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"type": "module",
"exports": {
"types": "./dist/types/index.d.ts",
"import": "./dist/index.es.js",
"default": "./dist/index.es.js"
},
"module": "dist/index.es.js",
"types": "dist/types/index.d.ts",
"repository": { "repository": {
"url": "https://github.com/status-im/status-web.git", "url": "https://github.com/status-im/status-web.git",
"directory": "packages/status-js", "directory": "packages/status-js",
@ -10,32 +18,16 @@
"bugs": { "bugs": {
"url": "https://github.com/status-im/status-web/issues" "url": "https://github.com/status-im/status-web/issues"
}, },
"source": "src/index.ts",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"targets": {
"main": {
"includeNodeModules": [
"protons-runtime",
"uint8arraylist"
]
}
},
"scripts": { "scripts": {
"prebuild": "rm -rf dist", "dev": "vite build --watch",
"dev": "parcel", "build": "vite build && yarn typegen",
"build": "parcel build", "test": "vitest",
"build:vite": "vite build",
"build:types": "tsc --emitDeclarationOnly",
"build:protos": "protons protos/*.proto",
"typecheck": "tsc --noEmit",
"lint": "eslint src", "lint": "eslint src",
"typecheck": "tsc",
"typegen": "tsc --noEmit false --emitDeclarationOnly --paths null || true",
"format": "prettier --write src", "format": "prettier --write src",
"clean": "rm -rf node_modules && rm -rf dist", "protos": "protons protos/*.proto",
"proto": "run-s 'proto:*'", "clean": "rm -rf dist node_modules .turbo"
"proto:lint": "buf lint",
"proto:build": "buf generate"
}, },
"dependencies": { "dependencies": {
"ethereum-cryptography": "^1.0.3", "ethereum-cryptography": "^1.0.3",
@ -45,7 +37,6 @@
"protons-runtime": "^1.0.4" "protons-runtime": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"npm-run-all": "^4.1.5",
"protons": "^3.0.4" "protons": "^3.0.4"
} }
} }

View File

@ -1,32 +1,31 @@
import { keccak256 } from 'ethereum-cryptography/keccak' import { keccak256 } from 'ethereum-cryptography/keccak'
import * as secp from 'ethereum-cryptography/secp256k1' import * as secp from 'ethereum-cryptography/secp256k1'
import { utf8ToBytes } from 'ethereum-cryptography/utils' import { utf8ToBytes } from 'ethereum-cryptography/utils'
import { expect, test } from 'vitest'
import { Account } from './account' import { Account } from './account'
describe('Account', () => { test('should verify the signature', async () => {
it('should verify the signature', async () => { const account = new Account()
const account = new Account()
const message = utf8ToBytes('123') const message = utf8ToBytes('123')
const messageHash = keccak256(message) const messageHash = keccak256(message)
const signature = await account.sign(message) const signature = await account.sign(message)
const signatureWithoutRecoveryId = signature.slice(0, -1) const signatureWithoutRecoveryId = signature.slice(0, -1)
expect( expect(
secp.verify(signatureWithoutRecoveryId, messageHash, account.publicKey) secp.verify(signatureWithoutRecoveryId, messageHash, account.publicKey)
).toBeTruthy() ).toBeTruthy()
}) })
it('should not verify signature with different message', async () => { test('should not verify signature with different message', async () => {
const account = new Account() const account = new Account()
const message = utf8ToBytes('123') const message = utf8ToBytes('123')
const messageHash = keccak256(message) const messageHash = keccak256(message)
const signature = await account.sign(utf8ToBytes('abc')) const signature = await account.sign(utf8ToBytes('abc'))
expect(secp.verify(signature, messageHash, account.publicKey)).toBeFalsy() expect(secp.verify(signature, messageHash, account.publicKey)).toBeFalsy()
})
}) })

View File

@ -1,25 +1,24 @@
import { PageDirection } from 'js-waku' import { PageDirection } from 'js-waku'
import { containsOnlyEmoji } from '../helpers/contains-only-emoji'
import { import {
AudioMessage, AudioMessage,
ChatMessage as ChatMessageProto, ChatMessage as ChatMessageProto,
DeleteMessage, DeleteMessage,
EditMessage, EditMessage,
ImageType, ImageType,
} from '~/protos/chat-message' } from '../protos/chat-message'
import { EmojiReaction } from '~/protos/emoji-reaction' import { EmojiReaction } from '../protos/emoji-reaction'
import { containsOnlyEmoji } from '../helpers/contains-only-emoji'
import { generateKeyFromPassword } from '../utils/generate-key-from-password' import { generateKeyFromPassword } from '../utils/generate-key-from-password'
import { idToContentTopic } from '../utils/id-to-content-topic' import { idToContentTopic } from '../utils/id-to-content-topic'
import { getReactions } from './community/get-reactions' import { getReactions } from './community/get-reactions'
import type { MessageType } from '../../protos/enums' import type { CommunityChat } from '../proto/communities/v1/communities'
import type { ImageMessage } from '../protos/chat-message'
import type { MessageType } from '../protos/enums'
import type { Client } from './client' import type { Client } from './client'
import type { Community } from './community/community' import type { Community } from './community/community'
import type { Reactions } from './community/get-reactions' import type { Reactions } from './community/get-reactions'
import type { ImageMessage } from '~/protos/chat-message'
import type { CommunityChat } from '~/src/proto/communities/v1/communities'
import type { WakuMessage } from 'js-waku' import type { WakuMessage } from 'js-waku'
export type ChatMessage = ChatMessageProto & { export type ChatMessage = ChatMessageProto & {

View File

@ -5,8 +5,7 @@
import { hexToBytes } from 'ethereum-cryptography/utils' import { hexToBytes } from 'ethereum-cryptography/utils'
import { Waku, WakuMessage } from 'js-waku' import { Waku, WakuMessage } from 'js-waku'
import { ApplicationMetadataMessage } from '~/protos/application-metadata-message' import { ApplicationMetadataMessage } from '../protos/application-metadata-message'
import { Account } from './account' import { Account } from './account'
import { Community } from './community/community' import { Community } from './community/community'
import { handleWakuMessage } from './community/handle-waku-message' import { handleWakuMessage } from './community/handle-waku-message'

View File

@ -1,22 +1,21 @@
import { hexToBytes } from 'ethereum-cryptography/utils' import { hexToBytes } from 'ethereum-cryptography/utils'
import { waku_message } from 'js-waku' import { waku_message } from 'js-waku'
import { CommunityRequestToJoin } from '~/protos/communities' import { getDifferenceByKeys } from '../../helpers/get-difference-by-keys'
import { MessageType } from '~/protos/enums' import { getObjectsDifference } from '../../helpers/get-objects-difference'
import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys' import { CommunityRequestToJoin } from '../../protos/communities'
import { getObjectsDifference } from '~/src/helpers/get-objects-difference' import { MessageType } from '../../protos/enums'
import { compressPublicKey } from '~/src/utils/compress-public-key' import { compressPublicKey } from '../../utils/compress-public-key'
import { generateKeyFromPassword } from '~/src/utils/generate-key-from-password' import { generateKeyFromPassword } from '../../utils/generate-key-from-password'
import { idToContentTopic } from '~/src/utils/id-to-content-topic' import { idToContentTopic } from '../../utils/id-to-content-topic'
import { Chat } from '../chat' import { Chat } from '../chat'
import { Member } from '../member' import { Member } from '../member'
import type { Client } from '../client'
import type { import type {
CommunityChat, CommunityChat,
CommunityDescription, CommunityDescription,
} from '~/src/proto/communities/v1/communities' } from '../../proto/communities/v1/communities'
import type { Client } from '../client'
export class Community { export class Community {
private client: Client private client: Client

View File

@ -1,4 +1,4 @@
import type { EmojiReaction } from '../../../protos/emoji-reaction' import type { EmojiReaction } from '../../protos/emoji-reaction'
type Reaction = Exclude<`${EmojiReaction.Type}`, 'UNKNOWN_EMOJI_REACTION_TYPE'> type Reaction = Exclude<`${EmojiReaction.Type}`, 'UNKNOWN_EMOJI_REACTION_TYPE'>

View File

@ -1,16 +1,16 @@
import { bytesToHex } from 'ethereum-cryptography/utils' import { bytesToHex } from 'ethereum-cryptography/utils'
import { ApplicationMetadataMessage } from '../../../protos/application-metadata-message' import { CommunityDescription } from '../../proto/communities/v1/communities'
import { ApplicationMetadataMessage } from '../../protos/application-metadata-message'
import { import {
ChatMessage, ChatMessage,
DeleteMessage, DeleteMessage,
EditMessage, EditMessage,
MessageType, MessageType,
} from '../../../protos/chat-message' } from '../../protos/chat-message'
import { EmojiReaction } from '../../../protos/emoji-reaction' import { EmojiReaction } from '../../protos/emoji-reaction'
import { PinMessage } from '../../../protos/pin-message' import { PinMessage } from '../../protos/pin-message'
import { ProtocolMessage } from '../../../protos/protocol-message' import { ProtocolMessage } from '../../protos/protocol-message'
import { CommunityDescription } from '../../proto/communities/v1/communities'
import { payloadToId } from '../../utils/payload-to-id' import { payloadToId } from '../../utils/payload-to-id'
import { recoverPublicKey } from '../../utils/recover-public-key' import { recoverPublicKey } from '../../utils/recover-public-key'
import { getChatUuid } from './get-chat-uuid' import { getChatUuid } from './get-chat-uuid'

View File

@ -1,5 +1,5 @@
import type { ChatMessage as ChatMessageProto } from '../../protos/chat-message'
import type { ChatMessage } from '../chat' import type { ChatMessage } from '../chat'
import type { ChatMessage as ChatMessageProto } from '~/protos/chat-message'
export function mapChatMessage( export function mapChatMessage(
decodedMessage: ChatMessageProto, decodedMessage: ChatMessageProto,

View File

@ -1,17 +1,17 @@
import { expect, test } from 'vitest'
import { containsOnlyEmoji } from './contains-only-emoji' import { containsOnlyEmoji } from './contains-only-emoji'
describe('containsOnlyEmoji', () => { test('should be truthy', () => {
it('should be truthy', () => { expect(containsOnlyEmoji('💩')).toBeTruthy()
expect(containsOnlyEmoji('💩')).toBeTruthy() expect(containsOnlyEmoji('💩💩💩💩💩💩')).toBeTruthy()
expect(containsOnlyEmoji('💩💩💩💩💩💩')).toBeTruthy() })
})
test('should be falsy', () => {
it('should be falsy', () => { expect(containsOnlyEmoji('')).toBeFalsy()
expect(containsOnlyEmoji('')).toBeFalsy() expect(containsOnlyEmoji(' ')).toBeFalsy()
expect(containsOnlyEmoji(' ')).toBeFalsy() expect(containsOnlyEmoji(' 💩')).toBeFalsy()
expect(containsOnlyEmoji(' 💩')).toBeFalsy() expect(containsOnlyEmoji('💩 ')).toBeFalsy()
expect(containsOnlyEmoji('💩 ')).toBeFalsy() expect(containsOnlyEmoji('text 💩')).toBeFalsy()
expect(containsOnlyEmoji('text 💩')).toBeFalsy() expect(containsOnlyEmoji('💩 text')).toBeFalsy()
expect(containsOnlyEmoji('💩 text')).toBeFalsy()
})
}) })

View File

@ -1,25 +1,25 @@
import { expect, test } from 'vitest'
import { getObjectsDifference } from './get-objects-difference' import { getObjectsDifference } from './get-objects-difference'
describe('getObjectsDifference', () => { test('should return correct difference', () => {
it('returns correct difference', () => { const oldObject = { a: 1, b: 2, c: 3 }
const oldObject = { a: 1, b: 2, c: 3 } const newObject = { c: 3, d: 4, e: 5 }
const newObject = { c: 3, d: 4, e: 5 }
expect(getObjectsDifference(oldObject, newObject)).toEqual({ expect(getObjectsDifference(oldObject, newObject)).toEqual({
added: { added: {
d: 4, d: 4,
e: 5, e: 5,
}, },
removed: ['a', 'b'], removed: ['a', 'b'],
}) })
}) })
it('returns empty arrays for the same object', () => { test('should return empty arrays for the same object', () => {
const object = { a: 1, b: 2, c: 3 } const object = { a: 1, b: 2, c: 3 }
expect(getObjectsDifference(object, object)).toEqual({ expect(getObjectsDifference(object, object)).toEqual({
added: {}, added: {},
removed: [], removed: [],
})
}) })
}) })

View File

@ -1,30 +1,29 @@
import { getPublicKey, utils } from 'ethereum-cryptography/secp256k1' import * as secp from 'ethereum-cryptography/secp256k1'
import { bytesToHex } from 'ethereum-cryptography/utils' import { bytesToHex } from 'ethereum-cryptography/utils'
import { expect, test } from 'vitest'
import { compressPublicKey } from './compress-public-key' import { compressPublicKey } from './compress-public-key'
describe('compressPublicKey', () => { test('should return compressed public key', () => {
it('should return compressed public key', () => { const privateKey = secp.utils.randomPrivateKey()
const privateKey = utils.randomPrivateKey()
const publicKey = bytesToHex(getPublicKey(privateKey)) const publicKey = bytesToHex(secp.getPublicKey(privateKey))
const compressedPublicKey = bytesToHex(getPublicKey(privateKey, true)) const compressedPublicKey = bytesToHex(secp.getPublicKey(privateKey, true))
expect(compressPublicKey(publicKey)).toEqual(compressedPublicKey) expect(compressPublicKey(publicKey)).toEqual(compressedPublicKey)
}) })
it('should accept public key with a base prefix', () => { test('should accept public key with a base prefix', () => {
const privateKey = utils.randomPrivateKey() const privateKey = secp.utils.randomPrivateKey()
const publicKey = '0x' + bytesToHex(getPublicKey(privateKey)) const publicKey = '0x' + bytesToHex(secp.getPublicKey(privateKey))
const compressedPublicKey = bytesToHex(getPublicKey(privateKey, true)) const compressedPublicKey = bytesToHex(secp.getPublicKey(privateKey, true))
expect(compressPublicKey(publicKey)).toEqual(compressedPublicKey) expect(compressPublicKey(publicKey)).toEqual(compressedPublicKey)
}) })
it('should throw error if public key is not a valid hex', () => { test('should throw error if public key is not a valid hex', () => {
expect(() => { expect(() => {
compressPublicKey('not a valid public key') compressPublicKey('not a valid public key')
}).toThrowErrorMatchingInlineSnapshot(`"Invalid public key"`) }).toThrowErrorMatchingInlineSnapshot(`"Invalid public key"`)
})
}) })

View File

@ -1,24 +1,23 @@
import { bytesToHex } from 'ethereum-cryptography/utils' import { bytesToHex } from 'ethereum-cryptography/utils'
import { expect, test } from 'vitest'
import { generateKeyFromPassword } from './generate-key-from-password' import { generateKeyFromPassword } from './generate-key-from-password'
describe('createSymKeyFromPassword', () => { test('should create symmetric key from password', async () => {
it('should create symmetric key from password', async () => { const password = 'arbitrary data here'
const password = 'arbitrary data here' const symKey = await generateKeyFromPassword(password)
const symKey = await generateKeyFromPassword(password)
expect(bytesToHex(symKey)).toEqual( expect(bytesToHex(symKey)).toEqual(
'c49ad65ebf2a7b7253bf400e3d27719362a91b2c9b9f54d50a69117021666c33' 'c49ad65ebf2a7b7253bf400e3d27719362a91b2c9b9f54d50a69117021666c33'
) )
}) })
it('should generate symmetric key from chat ID', async () => { test('should generate symmetric key from chat ID', async () => {
const chatId = const chatId =
'0x02dcec6041fb999d65f1d33363e08c93d3c1f6f0fbbb26add383e2cf46c2b921f41dc14fd8-9a8b-4df5-a358-2c3067be5439' '0x02dcec6041fb999d65f1d33363e08c93d3c1f6f0fbbb26add383e2cf46c2b921f41dc14fd8-9a8b-4df5-a358-2c3067be5439'
const symKey = await generateKeyFromPassword(chatId) const symKey = await generateKeyFromPassword(chatId)
expect(bytesToHex(symKey)).toEqual( expect(bytesToHex(symKey)).toEqual(
'76ff5bf0a74a8e724367c7fc003f066d477641f468768a8da2817addf5c2ce76' '76ff5bf0a74a8e724367c7fc003f066d477641f468768a8da2817addf5c2ce76'
) )
})
}) })

View File

@ -1,17 +1,17 @@
import { expect, test } from 'vitest'
import { generateUsername } from './generate-username' import { generateUsername } from './generate-username'
describe('generateUsername', () => { test('should generate the username', () => {
it('should generate the username', async () => { const publicKey1 =
const publicKey1 = '0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3'
'0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3' expect(generateUsername(publicKey1)).toBe('Darkorange Blue Bubblefish')
expect(generateUsername(publicKey1)).toBe('Darkorange Blue Bubblefish')
const publicKey2 = const publicKey2 =
'0x04ac419dac9a8bbb58825a3cde60eef0ee71b8cf6c63df611eeefc8e7aac7c79b55954b679d24cf5ec82da7ed921caf240628a9bfb3450c5111a9cffe54e631811' '0x04ac419dac9a8bbb58825a3cde60eef0ee71b8cf6c63df611eeefc8e7aac7c79b55954b679d24cf5ec82da7ed921caf240628a9bfb3450c5111a9cffe54e631811'
expect(generateUsername(publicKey2)).toBe('Bumpy Absolute Crustacean') expect(generateUsername(publicKey2)).toBe('Bumpy Absolute Crustacean')
const publicKey3 = const publicKey3 =
'0x0403aeff2fdd0044b136e06afa6d69bb563bb7b3fd518bb30c0d5115a2e020840a2247966c2cc9953ed02cc391e8883b3319f63a31e5f5369d0fb72b62b23dfcbd' '0x0403aeff2fdd0044b136e06afa6d69bb563bb7b3fd518bb30c0d5115a2e020840a2247966c2cc9953ed02cc391e8883b3319f63a31e5f5369d0fb72b62b23dfcbd'
expect(generateUsername(publicKey3)).toBe('Back Careful Cuckoo') expect(generateUsername(publicKey3)).toBe('Back Careful Cuckoo')
})
}) })

View File

@ -1,7 +1,7 @@
import { expect, test } from 'vitest'
import { idToContentTopic } from './id-to-content-topic' import { idToContentTopic } from './id-to-content-topic'
describe('idToContentTopic', () => { test('should return content topic', () => {
it('should return content topic', () => { expect(idToContentTopic).toBeDefined()
expect(idToContentTopic).toBeDefined()
})
}) })

View File

@ -1,9 +1,11 @@
import { expect, test } from 'vitest'
import { import {
hexToColorHash, hexToColorHash,
publicKeyToColorHash, publicKeyToColorHash,
} from './public-key-to-color-hash' } from './public-key-to-color-hash'
test('returns color hash from public key', () => { test('should return color hash from public key', () => {
expect( expect(
publicKeyToColorHash( publicKeyToColorHash(
'0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8' '0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8'
@ -23,29 +25,29 @@ test('returns color hash from public key', () => {
]) ])
}) })
test('returns undefined for invalid public keys', () => { test('should throw for invalid public keys', () => {
expect(publicKeyToColorHash('abc')).toBeUndefined() expect(() => publicKeyToColorHash('abc')).toThrow()
expect(publicKeyToColorHash('0x01')).toBeUndefined() expect(() => publicKeyToColorHash('0x01')).toThrow()
expect( expect(() =>
publicKeyToColorHash( publicKeyToColorHash(
'0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8' '0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8'
) )
).toBeUndefined() ).toThrow()
expect( expect(() =>
publicKeyToColorHash( publicKeyToColorHash(
'0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8' '0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8'
) )
).toBeUndefined() ).toThrow()
}) })
test('returns color hash from hex', () => { test('should return color hash from hex', () => {
expect(hexToColorHash('0', 4, 4)).toEqual([[1, 0]]) expect(hexToColorHash('0', 4, 4)).toEqual([[1, 0]])
expect(hexToColorHash('1', 4, 4)).toEqual([[1, 1]]) expect(hexToColorHash('1', 4, 4)).toEqual([[1, 1]])
expect(hexToColorHash('4', 4, 4)).toEqual([[2, 0]]) expect(hexToColorHash('4', 4, 4)).toEqual([[2, 0]])
expect(hexToColorHash('F', 4, 4)).toEqual([[4, 3]]) expect(hexToColorHash('F', 4, 4)).toEqual([[4, 3]])
}) })
test('returns color hash from hex with redecued collision resistance', () => { test('should return color hash from hex with reduced collision resistance', () => {
expect(hexToColorHash('FF', 4, 4)).toEqual([ expect(hexToColorHash('FF', 4, 4)).toEqual([
[4, 3], [4, 3],
[4, 0], [4, 0],

View File

@ -1,81 +1,79 @@
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils' import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils'
import { expect, test } from 'vitest'
import { Account } from '../client/account' import { Account } from '../client/account'
import { recoverPublicKey } from './recover-public-key' import { recoverPublicKey } from './recover-public-key'
import type { ApplicationMetadataMessage } from '../../protos/application-metadata-message' import type { ApplicationMetadataMessage } from '../protos/application-metadata-message'
describe('recoverPublicKey', () => { test('should recover public key', async () => {
it('should recover public key', async () => { const payload = utf8ToBytes('hello')
const payload = utf8ToBytes('hello')
const account = new Account() const account = new Account()
const signature = await account.sign(payload) const signature = await account.sign(payload)
expect(bytesToHex(recoverPublicKey(signature, payload))).toEqual( expect(bytesToHex(recoverPublicKey(signature, payload))).toEqual(
account.publicKey account.publicKey
) )
}) })
it('should recover public key from fixture', async () => { test('should recover public key from fixture', async () => {
const metadataFixture: ApplicationMetadataMessage = { const metadataFixture: ApplicationMetadataMessage = {
type: 'TYPE_EMOJI_REACTION' as ApplicationMetadataMessage.Type, type: 'TYPE_EMOJI_REACTION' as ApplicationMetadataMessage.Type,
signature: new Uint8Array([ signature: new Uint8Array([
250, 132, 234, 119, 159, 124, 98, 93, 197, 108, 99, 52, 186, 234, 142, 250, 132, 234, 119, 159, 124, 98, 93, 197, 108, 99, 52, 186, 234, 142,
101, 147, 180, 50, 190, 102, 61, 219, 189, 95, 124, 29, 74, 43, 46, 106, 101, 147, 180, 50, 190, 102, 61, 219, 189, 95, 124, 29, 74, 43, 46, 106,
108, 102, 234, 77, 209, 130, 140, 87, 96, 210, 34, 11, 115, 56, 98, 223, 108, 102, 234, 77, 209, 130, 140, 87, 96, 210, 34, 11, 115, 56, 98, 223,
154, 30, 239, 23, 197, 243, 196, 248, 63, 162, 20, 108, 84, 250, 150, 154, 30, 239, 23, 197, 243, 196, 248, 63, 162, 20, 108, 84, 250, 150, 230,
230, 129, 0, 129, 0,
]), ]),
payload: new Uint8Array([ payload: new Uint8Array([
8, 138, 245, 146, 158, 148, 48, 18, 104, 48, 120, 48, 50, 57, 102, 49, 8, 138, 245, 146, 158, 148, 48, 18, 104, 48, 120, 48, 50, 57, 102, 49, 57,
57, 54, 98, 98, 102, 101, 102, 52, 102, 97, 54, 97, 53, 101, 98, 56, 49, 54, 98, 98, 102, 101, 102, 52, 102, 97, 54, 97, 53, 101, 98, 56, 49, 100,
100, 100, 56, 48, 50, 49, 51, 51, 97, 54, 51, 52, 57, 56, 51, 50, 53, 100, 56, 48, 50, 49, 51, 51, 97, 54, 51, 52, 57, 56, 51, 50, 53, 52, 52,
52, 52, 53, 99, 97, 49, 97, 102, 49, 100, 49, 53, 52, 98, 49, 98, 98, 53, 99, 97, 49, 97, 102, 49, 100, 49, 53, 52, 98, 49, 98, 98, 52, 53, 52,
52, 53, 52, 50, 57, 53, 53, 49, 51, 51, 51, 48, 56, 48, 52, 101, 97, 55, 50, 57, 53, 53, 49, 51, 51, 51, 48, 56, 48, 52, 101, 97, 55, 45, 98, 100,
45, 98, 100, 54, 54, 45, 52, 100, 53, 100, 45, 57, 49, 101, 98, 45, 98, 54, 54, 45, 52, 100, 53, 100, 45, 57, 49, 101, 98, 45, 98, 50, 100, 99,
50, 100, 99, 102, 101, 50, 53, 49, 53, 98, 51, 26, 66, 48, 120, 53, 97, 102, 101, 50, 53, 49, 53, 98, 51, 26, 66, 48, 120, 53, 97, 57, 49, 99, 52,
57, 49, 99, 52, 54, 48, 97, 97, 100, 101, 99, 51, 99, 55, 54, 100, 48, 54, 48, 97, 97, 100, 101, 99, 51, 99, 55, 54, 100, 48, 56, 48, 98, 54, 99,
56, 48, 98, 54, 99, 55, 50, 97, 50, 48, 101, 49, 53, 97, 51, 51, 55, 55, 50, 97, 50, 48, 101, 49, 53, 97, 51, 51, 55, 102, 55, 99, 48, 98, 55,
102, 55, 99, 48, 98, 55, 55, 97, 55, 99, 48, 97, 53, 101, 98, 97, 53, 55, 97, 55, 99, 48, 97, 53, 101, 98, 97, 53, 102, 97, 57, 100, 52, 100,
102, 97, 57, 100, 52, 100, 57, 49, 98, 97, 56, 32, 5, 40, 2, 57, 49, 98, 97, 56, 32, 5, 40, 2,
]), ]),
} }
const publicKeySnapshot = new Uint8Array([ const publicKeySnapshot = new Uint8Array([
4, 172, 65, 157, 172, 154, 139, 187, 88, 130, 90, 60, 222, 96, 238, 240, 4, 172, 65, 157, 172, 154, 139, 187, 88, 130, 90, 60, 222, 96, 238, 240,
238, 113, 184, 207, 108, 99, 223, 97, 30, 238, 252, 142, 122, 172, 124, 238, 113, 184, 207, 108, 99, 223, 97, 30, 238, 252, 142, 122, 172, 124, 121,
121, 181, 89, 84, 182, 121, 210, 76, 245, 236, 130, 218, 126, 217, 33, 181, 89, 84, 182, 121, 210, 76, 245, 236, 130, 218, 126, 217, 33, 202, 242,
202, 242, 64, 98, 138, 155, 251, 52, 80, 197, 17, 26, 156, 255, 229, 78, 64, 98, 138, 155, 251, 52, 80, 197, 17, 26, 156, 255, 229, 78, 99, 24, 17,
99, 24, 17, ])
])
const result = recoverPublicKey(
const result = recoverPublicKey( metadataFixture.signature,
metadataFixture.signature, metadataFixture.payload
metadataFixture.payload )
)
expect(result).toEqual(publicKeySnapshot)
expect(result).toEqual(publicKeySnapshot) })
})
test('should not recover public key with different payload', async () => {
it('should not recover public key with different payload', async () => { const payload = utf8ToBytes('1')
const payload = utf8ToBytes('1')
const account = new Account()
const account = new Account() const signature = await account.sign(payload)
const signature = await account.sign(payload)
const payload2 = utf8ToBytes('2')
const payload2 = utf8ToBytes('2') expect(recoverPublicKey(signature, payload2)).not.toEqual(account.publicKey)
expect(recoverPublicKey(signature, payload2)).not.toEqual(account.publicKey) })
})
test('should throw error when signature length is not 65 bytes', async () => {
it('should throw error when signature length is not 65 bytes', async () => { const payload = utf8ToBytes('hello')
const payload = utf8ToBytes('hello') const signature = new Uint8Array([
const signature = new Uint8Array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, ])
])
expect(() =>
expect(() => recoverPublicKey(signature, payload)
recoverPublicKey(signature, payload) ).toThrowErrorMatchingInlineSnapshot(`"Signature must be 65 bytes long"`)
).toThrowErrorMatchingInlineSnapshot(`"Signature must be 65 bytes long"`)
})
}) })

View File

@ -1,4 +1,4 @@
import type { ChatMessage } from '~/protos/chat-message' import type { ChatMessage } from '../protos/chat-message'
// TODO?: maybe this should normalize the message? // TODO?: maybe this should normalize the message?
export function validateMessage(message: ChatMessage): boolean { export function validateMessage(message: ChatMessage): boolean {

View File

@ -2,11 +2,7 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"include": ["src"], "include": ["src"],
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "./dist",
"declarationDir": "dist/types", "declarationDir": "./dist/types"
"baseUrl": ".",
"paths": {
"~/*": ["./*"]
}
} }
} }

View File

@ -0,0 +1,37 @@
/// <reference types="vitest" />
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import { dependencies } from './package.json'
const external = [
...Object.keys(dependencies || {}),
// ...Object.keys(peerDependencies || {}),
].map(name => new RegExp(`^${name}(/.*)?`))
export default defineConfig(({ command }) => {
return {
resolve: {
alias: {
'~': resolve('.'),
},
},
build: {
lib: {
entry: './src/index.ts',
fileName: 'index',
formats: ['es'],
},
target: 'es2020',
emptyOutDir: command === 'build',
rollupOptions: {
external,
},
},
test: {
// environment: 'happy-dom',
},
}
})

View File

@ -2,7 +2,14 @@
"name": "@status-im/react", "name": "@status-im/react",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"homepage": "https://github.com/status-im/status-web", "type": "module",
"exports": {
"types": "./dist/types/index.d.ts",
"import": "./dist/index.es.js",
"default": "./dist/index.es.js"
},
"module": "dist/index.es.js",
"types": "dist/types/index.d.ts",
"repository": { "repository": {
"url": "https://github.com/status-im/status-web.git", "url": "https://github.com/status-im/status-web.git",
"directory": "packages/status-react", "directory": "packages/status-react",
@ -11,18 +18,15 @@
"bugs": { "bugs": {
"url": "https://github.com/status-im/status-web/issues" "url": "https://github.com/status-im/status-web/issues"
}, },
"source": "src/index.tsx",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/types.d.ts",
"scripts": { "scripts": {
"prebuild": "rm -rf dist", "dev": "vite build --watch",
"build": "parcel build", "build": "vite build && yarn typegen",
"build:types": "tsc --emitDeclarationOnly", "#test": "vitest",
"typecheck": "tsc --noEmit", "typecheck": "tsc",
"typegen": "tsc --noEmit false --emitDeclarationOnly --paths null || true",
"lint": "eslint src", "lint": "eslint src",
"format": "prettier --write src", "format": "prettier --write src",
"clean": "rm -rf node_modules && rm -rf dist" "clean": "rm -rf dist node_modules .turbo"
}, },
"dependencies": { "dependencies": {
"@hcaptcha/react-hcaptcha": "^1.0.0", "@hcaptcha/react-hcaptcha": "^1.0.0",
@ -45,9 +49,7 @@
"emoji-mart": "^3.0.1", "emoji-mart": "^3.0.1",
"html-entities": "^2.3.2", "html-entities": "^2.3.2",
"qrcode.react": "^3.0.1", "qrcode.react": "^3.0.1",
"react": "^17.0.2",
"react-content-loader": "^6.2.0", "react-content-loader": "^6.2.0",
"react-dom": "^17.0.2",
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"styled-components": "^5.3.1", "styled-components": "^5.3.1",
@ -58,7 +60,9 @@
"@types/hcaptcha__react-hcaptcha": "^0.1.5", "@types/hcaptcha__react-hcaptcha": "^0.1.5",
"@types/node": "^16.9.6", "@types/node": "^16.9.6",
"@types/react": "^17.0.16", "@types/react": "^17.0.16",
"@types/styled-components": "^5.1.12" "@types/styled-components": "^5.1.12",
"@vitejs/plugin-react": "^1.3.2",
"happy-dom": "^5.3.1"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0", "react": "^16.8.0 || ^17.0.0",

View File

@ -1,116 +0,0 @@
import { useEffect, useMemo, useReducer } from 'react'
import { useUserPublicKey } from '../contexts/identityProvider'
import { useMessengerContext } from '../contexts/messengerProvider'
import type { Activities, Activity, ActivityStatus } from '../models/Activity'
import type { ChatMessage } from '../models/ChatMessage'
export type ActivityAction =
| { type: 'addActivity'; payload: Activity }
| { type: 'removeActivity'; payload: 'string' }
| { type: 'setAsRead'; payload: string }
| { type: 'setAllAsRead' }
| { type: 'setStatus'; payload: { id: string; status: ActivityStatus } }
function activityReducer(
state: Activities,
action: ActivityAction
): Activities {
switch (action.type) {
case 'setStatus': {
const activity = state[action.payload.id]
if (activity && 'status' in activity) {
activity.status = action.payload.status
activity.isRead = true
return { ...state, [activity.id]: activity }
}
return state
}
case 'setAsRead': {
const activity = state[action.payload]
if (activity) {
activity.isRead = true
return { ...state, [activity.id]: activity }
}
return state
}
case 'setAllAsRead': {
return Object.entries(state).reduce((prev, curr) => {
const activity = curr[1]
activity.isRead = true
return { ...prev, [curr[0]]: activity }
}, {})
}
case 'addActivity': {
return { ...state, [action.payload.id]: action.payload }
}
case 'removeActivity': {
if (state[action.payload]) {
const newState = { ...state }
delete newState[action.payload]
return newState
} else {
return state
}
}
default:
throw new Error('Wrong activity reducer type')
}
}
export function useActivities() {
const [activitiesObj, dispatch] = useReducer(activityReducer, {})
const activities = useMemo(
() => Object.values(activitiesObj),
[activitiesObj]
)
const userPK = useUserPublicKey()
const { subscriptionsDispatch, channels } = useMessengerContext()
useEffect(() => {
if (userPK) {
const subscribeFunction = (message: ChatMessage, id: string) => {
if (message.quote && message.quote.sender === userPK) {
const newActivity: Activity = {
id: message.date.getTime().toString() + message.content,
type: 'reply',
date: message.date,
user: message.sender,
message: message,
channel: channels[id],
quote: message.quote,
}
dispatch({ type: 'addActivity', payload: newActivity })
}
const split = message.content.split(' ')
const userMentioned = split.some(
fragment => fragment.startsWith('@') && fragment.slice(1) == userPK
)
if (userMentioned) {
const newActivity: Activity = {
id: message.date.getTime().toString() + message.content,
type: 'mention',
date: message.date,
user: message.sender,
message: message,
channel: channels[id],
}
dispatch({ type: 'addActivity', payload: newActivity })
}
}
subscriptionsDispatch({
type: 'addSubscription',
payload: { name: 'activityCenter', subFunction: subscribeFunction },
})
}
return () =>
subscriptionsDispatch({
type: 'removeSubscription',
payload: { name: 'activityCenter' },
})
}, [subscriptionsDispatch, userPK, channels])
return { activities, activityDispatch: dispatch }
}

View File

@ -1,56 +0,0 @@
import { useEffect, useState } from 'react'
import { useMessengerContext } from '../contexts/messengerProvider'
import type { ChatMessage } from '../models/ChatMessage'
export function useChatScrollHandle(
messages: ChatMessage[],
ref: React.RefObject<HTMLHeadingElement>
) {
const { loadPrevDay, loadingMessages, activeChannel } = useMessengerContext()
const [scrollOnBot, setScrollOnBot] = useState(true)
useEffect(() => {
if (ref && ref.current && scrollOnBot) {
ref.current.scrollTop = ref.current.scrollHeight
}
}, [messages.length, scrollOnBot, ref])
useEffect(() => {
if (activeChannel) {
if (
(ref?.current?.clientHeight ?? 0) >= (ref?.current?.scrollHeight ?? 0)
) {
setScrollOnBot(true)
loadPrevDay(activeChannel.id, activeChannel.type !== 'channel')
}
}
}, [messages.length, activeChannel, loadPrevDay, setScrollOnBot, ref])
useEffect(() => {
const currentRef = ref.current
const setScroll = () => {
if (ref?.current && activeChannel) {
if (ref.current.scrollTop <= 0) {
loadPrevDay(activeChannel.id, activeChannel.type !== 'channel')
}
if (
ref.current.scrollTop + ref.current.clientHeight ==
ref.current.scrollHeight
) {
if (scrollOnBot === false) {
setScrollOnBot(true)
}
} else {
if (scrollOnBot === true) {
setScrollOnBot(false)
}
}
}
}
currentRef?.addEventListener('scroll', setScroll)
return () => currentRef?.removeEventListener('scroll', setScroll)
}, [ref, scrollOnBot, activeChannel, loadPrevDay])
return loadingMessages
}

View File

@ -1,40 +0,0 @@
import { useMemo } from 'react'
import { useMessengerContext } from '../contexts/messengerProvider'
export enum NameErrors {
NoError = 0,
NameExists = 1,
BadCharacters = 2,
EndingWithEth = 3,
TooLong = 4,
}
export function useNameError(name: string) {
const { contacts } = useMessengerContext()
const error = useMemo(() => {
const RegName = new RegExp('^[a-z0-9_-]+$')
if (name === '') {
return NameErrors.NoError
}
const nameExists = Object.values(contacts).find(
contact => contact.trueName === name
)
if (nameExists) {
return NameErrors.NameExists
}
if (!name.match(RegName)) {
return NameErrors.BadCharacters
}
if (name.slice(-4) === '_eth' || name.slice(-4) === '-eth') {
return NameErrors.EndingWithEth
}
if (name.length >= 24) {
return NameErrors.TooLong
}
return NameErrors.NoError
}, [name, contacts])
return error
}

View File

@ -1,26 +0,0 @@
import { useEffect, useState } from 'react'
export function useRefBreak(dimension: number, sizeThreshold: number) {
const [widthBreak, setWidthBreak] = useState(dimension < sizeThreshold)
useEffect(() => {
const checkDimensions = () => {
if (dimension && dimension < sizeThreshold && dimension > 0) {
if (widthBreak === false) {
setWidthBreak(true)
}
} else {
if (widthBreak === true) {
setWidthBreak(false)
}
}
}
checkDimensions()
window.addEventListener('resize', checkDimensions)
return () => {
window.removeEventListener('resize', checkDimensions)
}
}, [dimension, widthBreak, sizeThreshold])
return widthBreak
}

View File

@ -1,21 +0,0 @@
export function binarySetInsert<T>(
arr: T[],
val: T,
compFunc: (a: T, b: T) => boolean,
eqFunc: (a: T, b: T) => boolean
) {
let low = 0
let high = arr.length
while (low < high) {
const mid = (low + high) >> 1
if (compFunc(arr[mid], val)) {
low = mid + 1
} else {
high = mid
}
}
if (arr.length === low || !eqFunc(arr[low], val)) {
arr.splice(low, 0, val)
}
return arr
}

View File

@ -1,18 +0,0 @@
const copyToClipboard = async (pngBlob: Blob) => {
try {
await navigator.clipboard.write([
new ClipboardItem({
[pngBlob.type]: pngBlob,
}),
])
} catch (error) {
console.error(error)
}
}
//Images are already converted to png by useMessenger when received
export const copyImg = async (image: string) => {
const img = await fetch(image)
const imgBlob = await img.blob()
return copyToClipboard(imgBlob)
}

View File

@ -1,7 +0,0 @@
export function equalDate(a: Date, b: Date) {
return (
a.getDate() === b.getDate() &&
a.getMonth() === b.getMonth() &&
a.getFullYear() === b.getFullYear()
)
}

View File

@ -1,79 +0,0 @@
import { bufToHex, hexToBuf, Identity } from '@status-im/js'
export async function saveIdentity(identity: Identity, password: string) {
const salt = window.crypto.getRandomValues(new Uint8Array(16))
const wrapKey = await getWrapKey(password, salt)
const iv = window.crypto.getRandomValues(new Uint8Array(12))
const cipher = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
wrapKey,
identity.privateKey
)
const data = {
salt: bufToHex(salt),
iv: bufToHex(iv),
cipher: bufToHex(cipher),
}
localStorage.setItem('cipherIdentity', JSON.stringify(data))
}
export function loadEncryptedIdentity(): string | null {
return localStorage.getItem('cipherIdentity')
}
async function getWrapKey(password: string, salt: Uint8Array) {
const enc = new TextEncoder()
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
enc.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
)
return await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
)
}
export async function decryptIdentity(
encryptedIdentity: string,
password: string
): Promise<Identity | undefined> {
const data = JSON.parse(encryptedIdentity)
const salt = hexToBuf(data.salt)
const iv = hexToBuf(data.iv)
const cipherKeyPair = hexToBuf(data.cipher)
const key = await getWrapKey(password, salt)
try {
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
cipherKeyPair
)
return new Identity(new Uint8Array(decrypted))
} catch (e) {
return
}
}

View File

@ -1,10 +0,0 @@
export { binarySetInsert } from './binarySetInsert'
export { copyImg } from './copyImg'
export { equalDate } from './equalDate'
export {
decryptIdentity,
loadEncryptedIdentity,
saveIdentity,
} from './identityStorage'
export { reduceString } from './reduceString'
export { uintToImgUrl } from './uintToImgUrl'

View File

@ -1,9 +0,0 @@
export const paste = (elementId: string) => {
navigator.clipboard
.readText()
.then(
clipText =>
((<HTMLInputElement>document.getElementById(elementId)).value =
clipText)
)
}

View File

@ -1,9 +0,0 @@
export const reduceString = (
string: string,
limitBefore: number,
limitAfter: number
) => {
return `${string.substring(0, limitBefore)}...${string.substring(
string.length - limitAfter
)}`
}

View File

@ -1,4 +0,0 @@
export function uintToImgUrl(img: Uint8Array) {
const blob = new Blob([img], { type: 'image/png' })
return URL.createObjectURL(blob)
}

View File

@ -0,0 +1,46 @@
/// <reference types="vitest" />
import react from '@vitejs/plugin-react'
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import { dependencies, peerDependencies } from './package.json'
const external = [
...Object.keys(dependencies || {}),
...Object.keys(peerDependencies || {}),
].map(name => new RegExp(`^${name}(/.*)?`))
export default defineConfig(({ command }) => {
return {
resolve: {
alias: {
'~': resolve('.'),
},
},
build: {
lib: {
entry: './src/index.tsx',
fileName: 'index',
formats: ['es'],
},
emptyOutDir: command === 'build',
// sourcemap: true,
target: 'es2020',
rollupOptions: {
external,
},
},
plugins: [
react({
// jsxRuntime: 'classic',
}),
],
test: {
environment: 'happy-dom',
},
}
})

View File

@ -1,9 +1,11 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
// "extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"incremental": true, "incremental": true,
"target": "ES2020",
"module": "ES2020", "module": "ES2020",
"moduleResolution": "node", "moduleResolution": "node",
"target": "ES2020",
"jsx": "preserve", "jsx": "preserve",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
@ -13,7 +15,7 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"isolatedModules": true, "isolatedModules": true,
// "noEmit": true, "noEmit": true,
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,

27
turbo.json Normal file
View File

@ -0,0 +1,27 @@
{
"$schema": "https://turborepo.org/schema.json",
"baseBranch": "origin/main",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false
},
"lint": {
"outputs": []
},
"check": {
"outputs": []
},
"test": {
"outputs": [],
"dependsOn": []
},
"clean": {
"cache": false
}
},
"globalDependencies": ["tsconfig.base.json"]
}

4630
yarn.lock

File diff suppressed because it is too large Load Diff