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/
# Turborepo
.turbo

View File

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

33
.vscode/launch.json vendored
View File

@ -4,20 +4,27 @@
{
"type": "node",
"request": "launch",
"name": "Launch Client",
"program": "${workspaceFolder}/packages/status-js/src/index.ts",
"preLaunchTask": "npm: build:status-js",
"outFiles": ["${workspaceFolder}/packages/status-js/dist/index.js"],
"sourceMaps": true,
"name": "Debug Client",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**"],
"cwd": "${workspaceFolder}/packages/status-js",
"program": "${workspaceRoot}/node_modules/vite-node/dist/cli.mjs",
"args": ["src/index.ts"],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": [
"${workspaceFolder}/node_modules/**/*",
"<node_internals>/**/*.js"
],
"sourceMapPathOverrides": {
"packages/status-js/*": "${workspaceFolder}/packages/status-js/*"
}
"console": "integratedTerminal",
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"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",
"jestRunIt.debugTestLabel": "Debug",
"jestRunIt.runTestLabel": "Run"
"eslint.packageManager": "yarn",
"npm.packageManager": "yarn"
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
@ -19,6 +19,6 @@
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

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

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 { render } from 'react-dom'
import { Community } from '@status-im/react'
const App = () => {
return (
<Community
publicKey="<YOUR_COMMUNITY_KEY>"
theme="light"
/>
)
}
import { App } from './app'
render(
<StrictMode>

View File

@ -2,4 +2,5 @@ body,
html,
#root {
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": {
"protons-runtime": "./node_modules/protons-runtime/dist/src/index.js",
"uint8arraylist": "./node_modules/uint8arraylist/dist/src/index.js"
},
"type": "module",
"private": true,
"workspaces": [
"packages/*",
@ -11,30 +8,19 @@
"keywords": [],
"scripts": {
"prepare": "husky install",
"fix": "run-s 'fix:*' && wsrun -e -c -s fix",
"build": "wsrun -e -c -s build",
"build:status-js": "yarn workspace @status-im/js build",
"build:status-react": "yarn workspace @status-im/react build",
"lint": "eslint 'packages/**/*.{ts,tsx}'",
"lint:fix": "eslint 'packages/**/*.{ts,tsx}' --fix",
"lint:examples": "eslint 'examples/**/*.{ts,tsx}'",
"test": "turbo run test --filter=@status-im/* -- --run",
"dev": "turbo run dev --parallel --filter=@status-im/*",
"build": "turbo run build --filter=@status-im/*",
"lint": "turbo run lint --filter=@status-im/*",
"check": "turbo run check --filter=@status-im/*",
"format": "prettier --write .",
"typecheck": "wsrun -e -c -s typecheck",
"test": "jest"
"format:check": "prettier --check .",
"clean": "turbo run clean && rm -rf node_modules .parcel-cache"
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@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",
"@tsconfig/strictest": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"babel-jest": "^28.1.0",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-node": "^0.3.6",
@ -48,14 +34,13 @@
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"husky": "^7.0.4",
"jest": "^28.1.0",
"lint-staged": "^12.3.4",
"npm-run-all": "^4.1.5",
"parcel": "^2.6.0",
"prettier": "^2.5.1",
"prettier": "^2.7.1",
"turbo": "^1.3.1",
"typescript": "^4.5.5",
"wsrun": "^5.2.4",
"ts-jest": "^28.0.4"
"vite": "^2.9.12",
"vite-node": "^0.16.0",
"vitest": "^0.16.0"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
@ -66,5 +51,9 @@
"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",
"version": "0.0.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": {
"url": "https://github.com/status-im/status-web.git",
"directory": "packages/status-js",
@ -10,32 +18,16 @@
"bugs": {
"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": {
"prebuild": "rm -rf dist",
"dev": "parcel",
"build": "parcel build",
"build:vite": "vite build",
"build:types": "tsc --emitDeclarationOnly",
"build:protos": "protons protos/*.proto",
"typecheck": "tsc --noEmit",
"dev": "vite build --watch",
"build": "vite build && yarn typegen",
"test": "vitest",
"lint": "eslint src",
"typecheck": "tsc",
"typegen": "tsc --noEmit false --emitDeclarationOnly --paths null || true",
"format": "prettier --write src",
"clean": "rm -rf node_modules && rm -rf dist",
"proto": "run-s 'proto:*'",
"proto:lint": "buf lint",
"proto:build": "buf generate"
"protos": "protons protos/*.proto",
"clean": "rm -rf dist node_modules .turbo"
},
"dependencies": {
"ethereum-cryptography": "^1.0.3",
@ -45,7 +37,6 @@
"protons-runtime": "^1.0.4"
},
"devDependencies": {
"npm-run-all": "^4.1.5",
"protons": "^3.0.4"
}
}

View File

@ -1,11 +1,11 @@
import { keccak256 } from 'ethereum-cryptography/keccak'
import * as secp from 'ethereum-cryptography/secp256k1'
import { utf8ToBytes } from 'ethereum-cryptography/utils'
import { expect, test } from 'vitest'
import { Account } from './account'
describe('Account', () => {
it('should verify the signature', async () => {
test('should verify the signature', async () => {
const account = new Account()
const message = utf8ToBytes('123')
@ -19,7 +19,7 @@ describe('Account', () => {
).toBeTruthy()
})
it('should not verify signature with different message', async () => {
test('should not verify signature with different message', async () => {
const account = new Account()
const message = utf8ToBytes('123')
@ -29,4 +29,3 @@ describe('Account', () => {
expect(secp.verify(signature, messageHash, account.publicKey)).toBeFalsy()
})
})

View File

@ -1,25 +1,24 @@
import { PageDirection } from 'js-waku'
import { containsOnlyEmoji } from '../helpers/contains-only-emoji'
import {
AudioMessage,
ChatMessage as ChatMessageProto,
DeleteMessage,
EditMessage,
ImageType,
} from '~/protos/chat-message'
import { EmojiReaction } from '~/protos/emoji-reaction'
import { containsOnlyEmoji } from '../helpers/contains-only-emoji'
} from '../protos/chat-message'
import { EmojiReaction } from '../protos/emoji-reaction'
import { generateKeyFromPassword } from '../utils/generate-key-from-password'
import { idToContentTopic } from '../utils/id-to-content-topic'
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 { Community } from './community/community'
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'
export type ChatMessage = ChatMessageProto & {

View File

@ -5,8 +5,7 @@
import { hexToBytes } from 'ethereum-cryptography/utils'
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 { Community } from './community/community'
import { handleWakuMessage } from './community/handle-waku-message'

View File

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

View File

@ -1,16 +1,16 @@
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 {
ChatMessage,
DeleteMessage,
EditMessage,
MessageType,
} from '../../../protos/chat-message'
import { EmojiReaction } from '../../../protos/emoji-reaction'
import { PinMessage } from '../../../protos/pin-message'
import { ProtocolMessage } from '../../../protos/protocol-message'
import { CommunityDescription } from '../../proto/communities/v1/communities'
} from '../../protos/chat-message'
import { EmojiReaction } from '../../protos/emoji-reaction'
import { PinMessage } from '../../protos/pin-message'
import { ProtocolMessage } from '../../protos/protocol-message'
import { payloadToId } from '../../utils/payload-to-id'
import { recoverPublicKey } from '../../utils/recover-public-key'
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 as ChatMessageProto } from '~/protos/chat-message'
export function mapChatMessage(
decodedMessage: ChatMessageProto,

View File

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

View File

@ -1,7 +1,8 @@
import { expect, test } from 'vitest'
import { getObjectsDifference } from './get-objects-difference'
describe('getObjectsDifference', () => {
it('returns correct difference', () => {
test('should return correct difference', () => {
const oldObject = { a: 1, b: 2, c: 3 }
const newObject = { c: 3, d: 4, e: 5 }
@ -14,7 +15,7 @@ describe('getObjectsDifference', () => {
})
})
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 }
expect(getObjectsDifference(object, object)).toEqual({
@ -22,4 +23,3 @@ describe('getObjectsDifference', () => {
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 { expect, test } from 'vitest'
import { compressPublicKey } from './compress-public-key'
describe('compressPublicKey', () => {
it('should return compressed public key', () => {
const privateKey = utils.randomPrivateKey()
test('should return compressed public key', () => {
const privateKey = secp.utils.randomPrivateKey()
const publicKey = bytesToHex(getPublicKey(privateKey))
const compressedPublicKey = bytesToHex(getPublicKey(privateKey, true))
const publicKey = bytesToHex(secp.getPublicKey(privateKey))
const compressedPublicKey = bytesToHex(secp.getPublicKey(privateKey, true))
expect(compressPublicKey(publicKey)).toEqual(compressedPublicKey)
})
it('should accept public key with a base prefix', () => {
const privateKey = utils.randomPrivateKey()
test('should accept public key with a base prefix', () => {
const privateKey = secp.utils.randomPrivateKey()
const publicKey = '0x' + bytesToHex(getPublicKey(privateKey))
const compressedPublicKey = bytesToHex(getPublicKey(privateKey, true))
const publicKey = '0x' + bytesToHex(secp.getPublicKey(privateKey))
const compressedPublicKey = bytesToHex(secp.getPublicKey(privateKey, true))
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(() => {
compressPublicKey('not a valid public key')
}).toThrowErrorMatchingInlineSnapshot(`"Invalid public key"`)
})
})

View File

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

View File

@ -1,7 +1,8 @@
import { expect, test } from 'vitest'
import { generateUsername } from './generate-username'
describe('generateUsername', () => {
it('should generate the username', async () => {
test('should generate the username', () => {
const publicKey1 =
'0x04eedbaafd6adf4a9233a13e7b1c3c14461fffeba2e9054b8d456ce5f6ebeafadcbf3dce3716253fbc391277fa5a086b60b283daf61fb5b1f26895f456c2f31ae3'
expect(generateUsername(publicKey1)).toBe('Darkorange Blue Bubblefish')
@ -14,4 +15,3 @@ describe('generateUsername', () => {
'0x0403aeff2fdd0044b136e06afa6d69bb563bb7b3fd518bb30c0d5115a2e020840a2247966c2cc9953ed02cc391e8883b3319f63a31e5f5369d0fb72b62b23dfcbd'
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'
describe('idToContentTopic', () => {
it('should return content topic', () => {
test('should return content topic', () => {
expect(idToContentTopic).toBeDefined()
})
})

View File

@ -1,9 +1,11 @@
import { expect, test } from 'vitest'
import {
hexToColorHash,
publicKeyToColorHash,
} from './public-key-to-color-hash'
test('returns color hash from public key', () => {
test('should return color hash from public key', () => {
expect(
publicKeyToColorHash(
'0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8'
@ -23,29 +25,29 @@ test('returns color hash from public key', () => {
])
})
test('returns undefined for invalid public keys', () => {
expect(publicKeyToColorHash('abc')).toBeUndefined()
expect(publicKeyToColorHash('0x01')).toBeUndefined()
expect(
test('should throw for invalid public keys', () => {
expect(() => publicKeyToColorHash('abc')).toThrow()
expect(() => publicKeyToColorHash('0x01')).toThrow()
expect(() =>
publicKeyToColorHash(
'0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8'
)
).toBeUndefined()
expect(
).toThrow()
expect(() =>
publicKeyToColorHash(
'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('1', 4, 4)).toEqual([[1, 1]])
expect(hexToColorHash('4', 4, 4)).toEqual([[2, 0]])
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([
[4, 3],
[4, 0],

View File

@ -1,12 +1,12 @@
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils'
import { expect, test } from 'vitest'
import { Account } from '../client/account'
import { recoverPublicKey } from './recover-public-key'
import type { ApplicationMetadataMessage } from '../../protos/application-metadata-message'
import type { ApplicationMetadataMessage } from '../protos/application-metadata-message'
describe('recoverPublicKey', () => {
it('should recover public key', async () => {
test('should recover public key', async () => {
const payload = utf8ToBytes('hello')
const account = new Account()
@ -17,37 +17,36 @@ describe('recoverPublicKey', () => {
)
})
it('should recover public key from fixture', async () => {
test('should recover public key from fixture', async () => {
const metadataFixture: ApplicationMetadataMessage = {
type: 'TYPE_EMOJI_REACTION' as ApplicationMetadataMessage.Type,
signature: new Uint8Array([
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,
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,
230, 129, 0,
154, 30, 239, 23, 197, 243, 196, 248, 63, 162, 20, 108, 84, 250, 150, 230,
129, 0,
]),
payload: new Uint8Array([
8, 138, 245, 146, 158, 148, 48, 18, 104, 48, 120, 48, 50, 57, 102, 49,
57, 54, 98, 98, 102, 101, 102, 52, 102, 97, 54, 97, 53, 101, 98, 56, 49,
100, 100, 56, 48, 50, 49, 51, 51, 97, 54, 51, 52, 57, 56, 51, 50, 53,
52, 52, 53, 99, 97, 49, 97, 102, 49, 100, 49, 53, 52, 98, 49, 98, 98,
52, 53, 52, 50, 57, 53, 53, 49, 51, 51, 51, 48, 56, 48, 52, 101, 97, 55,
45, 98, 100, 54, 54, 45, 52, 100, 53, 100, 45, 57, 49, 101, 98, 45, 98,
50, 100, 99, 102, 101, 50, 53, 49, 53, 98, 51, 26, 66, 48, 120, 53, 97,
57, 49, 99, 52, 54, 48, 97, 97, 100, 101, 99, 51, 99, 55, 54, 100, 48,
56, 48, 98, 54, 99, 55, 50, 97, 50, 48, 101, 49, 53, 97, 51, 51, 55,
102, 55, 99, 48, 98, 55, 55, 97, 55, 99, 48, 97, 53, 101, 98, 97, 53,
102, 97, 57, 100, 52, 100, 57, 49, 98, 97, 56, 32, 5, 40, 2,
8, 138, 245, 146, 158, 148, 48, 18, 104, 48, 120, 48, 50, 57, 102, 49, 57,
54, 98, 98, 102, 101, 102, 52, 102, 97, 54, 97, 53, 101, 98, 56, 49, 100,
100, 56, 48, 50, 49, 51, 51, 97, 54, 51, 52, 57, 56, 51, 50, 53, 52, 52,
53, 99, 97, 49, 97, 102, 49, 100, 49, 53, 52, 98, 49, 98, 98, 52, 53, 52,
50, 57, 53, 53, 49, 51, 51, 51, 48, 56, 48, 52, 101, 97, 55, 45, 98, 100,
54, 54, 45, 52, 100, 53, 100, 45, 57, 49, 101, 98, 45, 98, 50, 100, 99,
102, 101, 50, 53, 49, 53, 98, 51, 26, 66, 48, 120, 53, 97, 57, 49, 99, 52,
54, 48, 97, 97, 100, 101, 99, 51, 99, 55, 54, 100, 48, 56, 48, 98, 54, 99,
55, 50, 97, 50, 48, 101, 49, 53, 97, 51, 51, 55, 102, 55, 99, 48, 98, 55,
55, 97, 55, 99, 48, 97, 53, 101, 98, 97, 53, 102, 97, 57, 100, 52, 100,
57, 49, 98, 97, 56, 32, 5, 40, 2,
]),
}
const publicKeySnapshot = new Uint8Array([
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,
121, 181, 89, 84, 182, 121, 210, 76, 245, 236, 130, 218, 126, 217, 33,
202, 242, 64, 98, 138, 155, 251, 52, 80, 197, 17, 26, 156, 255, 229, 78,
99, 24, 17,
238, 113, 184, 207, 108, 99, 223, 97, 30, 238, 252, 142, 122, 172, 124, 121,
181, 89, 84, 182, 121, 210, 76, 245, 236, 130, 218, 126, 217, 33, 202, 242,
64, 98, 138, 155, 251, 52, 80, 197, 17, 26, 156, 255, 229, 78, 99, 24, 17,
])
const result = recoverPublicKey(
@ -58,7 +57,7 @@ describe('recoverPublicKey', () => {
expect(result).toEqual(publicKeySnapshot)
})
it('should not recover public key with different payload', async () => {
test('should not recover public key with different payload', async () => {
const payload = utf8ToBytes('1')
const account = new Account()
@ -68,7 +67,7 @@ describe('recoverPublicKey', () => {
expect(recoverPublicKey(signature, payload2)).not.toEqual(account.publicKey)
})
it('should throw error when signature length is not 65 bytes', async () => {
test('should throw error when signature length is not 65 bytes', async () => {
const payload = utf8ToBytes('hello')
const signature = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
@ -78,4 +77,3 @@ describe('recoverPublicKey', () => {
recoverPublicKey(signature, payload)
).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?
export function validateMessage(message: ChatMessage): boolean {

View File

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

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",
"version": "0.0.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": {
"url": "https://github.com/status-im/status-web.git",
"directory": "packages/status-react",
@ -11,18 +18,15 @@
"bugs": {
"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": {
"prebuild": "rm -rf dist",
"build": "parcel build",
"build:types": "tsc --emitDeclarationOnly",
"typecheck": "tsc --noEmit",
"dev": "vite build --watch",
"build": "vite build && yarn typegen",
"#test": "vitest",
"typecheck": "tsc",
"typegen": "tsc --noEmit false --emitDeclarationOnly --paths null || true",
"lint": "eslint src",
"format": "prettier --write src",
"clean": "rm -rf node_modules && rm -rf dist"
"clean": "rm -rf dist node_modules .turbo"
},
"dependencies": {
"@hcaptcha/react-hcaptcha": "^1.0.0",
@ -45,9 +49,7 @@
"emoji-mart": "^3.0.1",
"html-entities": "^2.3.2",
"qrcode.react": "^3.0.1",
"react": "^17.0.2",
"react-content-loader": "^6.2.0",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-router-dom": "^6.3.0",
"styled-components": "^5.3.1",
@ -58,7 +60,9 @@
"@types/hcaptcha__react-hcaptcha": "^0.1.5",
"@types/node": "^16.9.6",
"@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": {
"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": {
"incremental": true,
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"target": "ES2020",
"jsx": "preserve",
"declaration": true,
"declarationMap": true,
@ -13,7 +15,7 @@
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
// "noEmit": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": 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