From 54ff47024ff11b169991135a1be4d05ae97542d2 Mon Sep 17 00:00:00 2001 From: neverthirty Date: Fri, 10 Nov 2023 14:03:09 +0400 Subject: [PATCH] add TON support --- .github/workflows/package-and-publish.yml | 196 --------------- .gitignore | 4 +- build-magic-sources.sh | 4 + package.json | 3 + src/api/gramjs/apiBuilders/messages.ts | 26 +- src/api/types/messages.ts | 3 +- src/assets/ton-gem.png | Bin 0 -> 15804 bytes src/bundles/extra.ts | 1 + src/components/common/Composer.tsx | 10 + src/components/middle/ActionMessage.tsx | 23 +- src/components/middle/composer/AttachMenu.tsx | 15 +- .../middle/composer/TonModal.async.tsx | 18 ++ src/components/middle/composer/TonModal.scss | 83 +++++++ src/components/middle/composer/TonModal.tsx | 229 ++++++++++++++++++ src/components/ui/Button.tsx | 2 + src/components/ui/InputText.tsx | 2 +- src/config.ts | 6 + src/global/actions/all.ts | 1 + src/global/actions/api/ton.ts | 101 ++++++++ src/global/cache.ts | 5 + src/global/helpers/messages.ts | 9 +- src/global/initialState.ts | 4 + src/global/types.ts | 20 +- src/styles/index.scss | 8 + webpack.config.ts | 10 +- 25 files changed, 575 insertions(+), 208 deletions(-) delete mode 100644 .github/workflows/package-and-publish.yml create mode 100755 build-magic-sources.sh create mode 100644 src/assets/ton-gem.png create mode 100644 src/components/middle/composer/TonModal.async.tsx create mode 100644 src/components/middle/composer/TonModal.scss create mode 100644 src/components/middle/composer/TonModal.tsx create mode 100644 src/global/actions/api/ton.ts diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml deleted file mode 100644 index 060e52b8b9..0000000000 --- a/.github/workflows/package-and-publish.yml +++ /dev/null @@ -1,196 +0,0 @@ -# Terms: -# "build" - Compile web project using webpack. -# "package" - Produce a distributive package for a specific platform as a workflow artifact. -# "publish" - Send a package to corresponding store and GitHub release page. -# "release" - build + package + publish -# -# Jobs in this workflow will skip the "publish" step when `PUBLISH_REPO` is not set. - -name: Package and publish - -on: - workflow_dispatch: - push: - branches: master - -env: - APP_NAME: Telegram A - -jobs: - electron-release: - name: Build, package and publish Electron - runs-on: macOS-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Use Node.js 20.x - uses: actions/setup-node@v3 - with: - node-version: 20.x - - - name: Cache node modules - id: npm-cache - uses: actions/cache@v3 - with: - path: node_modules - key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build- - - - name: Install dependencies - if: steps.npm-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Import MacOS signing certificate - env: - APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - run: | - KEY_CHAIN=build.keychain - CERTIFICATE_P12=certificate.p12 - echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > $CERTIFICATE_P12 - security create-keychain -p actions $KEY_CHAIN - security default-keychain -s $KEY_CHAIN - security unlock-keychain -p actions $KEY_CHAIN - security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions $KEY_CHAIN - security find-identity -v -p codesigning $KEY_CHAIN - - - name: Get branch name for current workflow run - id: branch-name - uses: tj-actions/branch-names@v7 - - - name: Build, package and publish - env: - TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }} - TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }} - - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - - GH_TOKEN: ${{ secrets.GH_TOKEN }} - PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} - BASE_URL: ${{ vars.BASE_URL }} - IS_PREVIEW: ${{ steps.branch-name.outputs.current_branch != 'master' }} - run: | - if [ -z "$PUBLISH_REPO" ]; then - npm run electron:package:staging - else - npm run electron:release:production - fi - - - uses: actions/upload-artifact@v3 - with: - name: ${{ env.APP_NAME }}-x64.dmg - path: dist-electron/${{ env.APP_NAME }}-x64.dmg - - - uses: actions/upload-artifact@v3 - with: - name: ${{ env.APP_NAME }}-arm64.dmg - path: dist-electron/${{ env.APP_NAME }}-arm64.dmg - - - uses: actions/upload-artifact@v3 - with: - name: ${{ env.APP_NAME }}-x86_64.AppImage - path: dist-electron/${{ env.APP_NAME }}-x86_64.AppImage - - - uses: actions/upload-artifact@v3 - with: - name: ${{ env.APP_NAME }}-x64.exe - path: dist-electron/${{ env.APP_NAME }}-x64.exe - - electron-sign-for-windows: - name: Sign and re-publish Windows package - needs: electron-release - runs-on: windows-latest - if: vars.PUBLISH_REPO != '' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup certificate - shell: bash - run: echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 - - - name: Set environment variables - id: variables - shell: bash - run: | - echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - echo "FILE_NAME=${{ env.APP_NAME }}-x64.exe" >> "$GITHUB_ENV" - echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" - echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV" - echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" - echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV" - echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH - echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH - echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH - - - name: Setup SSM KSP - env: - SM_API_KEY: ${{ secrets.SM_API_KEY }} - shell: cmd - run: | - curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools.msi - msiexec /i smtools.msi /quiet /qn - smksp_registrar.exe list - smctl.exe keypair ls - C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user - smksp_cert_sync.exe - - - name: Download Windows package - id: download-artifact - uses: actions/download-artifact@v3 - with: - name: ${{ env.FILE_NAME }} - - - name: Sign package - env: - KEYPAIR_ALIAS: ${{ secrets.KEYPAIR_ALIAS }} - FILE_PATH: ${{ steps.download-artifact.outputs.download-path }} - shell: cmd - run: smctl.exe sign --keypair-alias=%KEYPAIR_ALIAS% --input "%FILE_PATH%\%FILE_NAME%" - - - uses: actions/upload-artifact@v3 - with: - name: ${{ env.FILE_NAME }} - path: ${{ env.FILE_NAME }} - - - name: Get latest release ID - id: release-id - shell: bash - run: | - RELEASE_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases?per_page=1" | jq -r '.[0].id') - echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT - - - name: Delete existing asset - env: - RELEASE_ID: ${{ steps.release-id.outputs.release_id }} - shell: bash - run: | - PUBLISH_FILE_NAME=${FILE_NAME// /-} # Consistency with electron-builder - ASSET_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets" | jq -r --arg PUBLISH_FILE_NAME "$PUBLISH_FILE_NAME" '.[] | select(.name == $PUBLISH_FILE_NAME) | .id') - curl -X DELETE -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/assets/$ASSET_ID" - - - name: Push new asset - env: - FILE_PATH: ${{ steps.download-artifact.outputs.download-path }} - RELEASE_ID: ${{ steps.release-id.outputs.release_id }} - shell: bash - run: | - PUBLISH_FILE_NAME=${FILE_NAME// /-} # Consistency with electron-builder - curl -X POST -H "Authorization: Bearer $GH_TOKEN" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@$FILE_PATH\\$FILE_NAME" \ - "https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=$PUBLISH_FILE_NAME" - - - name: Publish release - env: - RELEASE_ID: ${{ steps.release-id.outputs.release_id }} - shell: bash - run: | - curl -X PATCH -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID" -d '{"draft": false}' diff --git a/.gitignore b/.gitignore index 242f04f68f..856255a243 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ tests/ *.iml dev/perf/screenshot* .DS_store -.github/workflows/* -!.github/workflows/package-and-publish.yml +dist/ +.github/ diff --git a/build-magic-sources.sh b/build-magic-sources.sh new file mode 100755 index 0000000000..679e7effb3 --- /dev/null +++ b/build-magic-sources.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +filenames=$(ls docs/ | grep -E "(\.[0-9a-f]{20}.*\.(css|js)$)|ton-gem.*" | while read line; do echo "\"$line\","; done | tr -d '\n') +echo "[${filenames%?}]" > docs/magic-sources.json diff --git a/package.json b/package.json index c71e79cf7f..824b2ded8b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "electron:package:staging": "ENV=staging npm run electron:package -- -p never", "electron:release:production": "ENV=production npm run electron:package -- -p always", "telegraph:update_changelog": "node ./dev/telegraphChangelog.js", + "use_version": "echo \"$(node -p -e \"require('./package.json').version.match(/^\\d+\\.\\d+/)[0]\").$(cat .patch-version)\"", + "build:ton": "npm i && rm -rf docs/ && APP_NAME=\"Telegram WebA+TON\" APP_VERSION=$(npm run use_version --silent) APP_ENV=production webpack --output-path docs && ./deploy/copy_to_dist.sh docs && ./build-magic-sources.sh", + "deploy:ton": "git reset --hard HEAD^ && git fetch upstream && git rebase upstream/master && npm run build:ton && git add -A && git commit -a -m '[Build for TON]' --no-verify && git push -f", "check": "tsc && stylelint \"**/*.{css,scss}\" && eslint . --ext .ts,.tsx,.js --ignore-pattern src/lib/gramjs", "check:fix": "npm run check -- --fix", "tl:rehash": "node ./dev/tlHash.js", diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 8f1fe5d886..11ad57c2c4 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -39,6 +39,8 @@ import { SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, + TON_MSG_ADDRESS_REQUEST, + TON_MSG_ADDRESS_RESPONSE, } from '../../../config'; import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage'; import { omitUndefined, pick } from '../../../util/iteratees'; @@ -173,8 +175,9 @@ export function buildApiMessageWithChatId( const content = buildMessageContent(mtpMessage); const action = mtpMessage.action && buildAction(mtpMessage.action, fromId, peerId, Boolean(mtpMessage.post), isOutgoing); - if (action) { - content.action = action; + const tonAction = mtpMessage.message ? buildTonAction(mtpMessage.message, isOutgoing) : undefined; + if (action || tonAction) { + content.action = action || tonAction; } const isScheduled = mtpMessage.date > getServerTime() + MIN_SCHEDULED_PERIOD; @@ -589,6 +592,24 @@ function buildAction( }; } +export function buildTonAction(text: string, isOutgoing: boolean): ApiAction | undefined { + if (text === TON_MSG_ADDRESS_REQUEST) { + return { + text: isOutgoing ? 'You requested TON address' : 'Your TON address was requested', + type: 'tonAddressRequest', + translationValues: [], + }; + } else if (text.startsWith(TON_MSG_ADDRESS_RESPONSE)) { + return { + text: isOutgoing ? 'Your TON address was shared' : 'TON address was shared with you', + type: 'tonAddressResponse', + translationValues: [], + }; + } + + return undefined; +} + function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: boolean): ApiReplyKeyboard | undefined { const { replyMarkup, media } = message; @@ -782,6 +803,7 @@ export function buildLocalMessage( ...(poll && buildNewPoll(poll, localId)), ...(contact && { contact }), ...(story && { storyData: story }), + action: text ? buildTonAction(text, true) : undefined, }, date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(), isOutgoing: !isChannel, diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 4f7b87636f..524da36245 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -290,7 +290,8 @@ export interface ApiAction { text: string; targetUserIds?: string[]; targetChatId?: string; - type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'suggestProfilePhoto' | 'other'; + type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'suggestProfilePhoto' | 'other' + | 'tonAddressRequest' | 'tonAddressResponse'; photo?: ApiPhoto; amount?: number; currency?: string; diff --git a/src/assets/ton-gem.png b/src/assets/ton-gem.png new file mode 100644 index 0000000000000000000000000000000000000000..d0aed5f6c4b9faf657df3358aa212befcc912448 GIT binary patch literal 15804 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 literal 0 HcmV?d00001 diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 0dcc5e7cc6..0e5416d55f 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -55,6 +55,7 @@ export { default as ReactionPicker } from '../components/middle/message/Reaction export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal'; export { default as PollModal } from '../components/middle/composer/PollModal'; +export { default as TonModal } from '../components/middle/composer/TonModal'; export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; export { default as BotCommandTooltip } from '../components/middle/composer/BotCommandTooltip'; export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu'; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index f4e1468d7c..d0fe7e810a 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -148,6 +148,7 @@ import PollModal from '../middle/composer/PollModal.async'; import SendAsMenu from '../middle/composer/SendAsMenu.async'; import StickerTooltip from '../middle/composer/StickerTooltip.async'; import SymbolMenuButton from '../middle/composer/SymbolMenuButton'; +import TonModal from '../middle/composer/TonModal.async'; import WebPagePreview from '../middle/composer/WebPagePreview'; import ReactionSelector from '../middle/message/ReactionSelector'; import Button from '../ui/Button'; @@ -410,6 +411,8 @@ const Composer: FC = ({ setIsMounted(true); }, MOUNT_ANIMATION_DURATION); + const [isTonModalOpen, openTonModal, closeTonModal] = useFlag(); + useEffect(() => { if (isInMessageList) return; @@ -1531,6 +1534,11 @@ const Composer: FC = ({ onClear={closePollModal} onSend={handlePollSend} /> + {renderedEditedMessage && ( = ({ theme={theme} onMenuOpen={onAttachMenuOpen} onMenuClose={onAttachMenuClose} + canSendTons={!isChatWithSelf && isUserId(chatId)} + onSendTons={openTonModal} /> {isInMessageList && Boolean(botKeyboardMessageId) && ( = ({ observeIntersectionForPlaying, onPinnedIntersectionChange, }) => { - const { openPremiumModal, requestConfetti, checkGiftCode } = getActions(); + const { + openPremiumModal, + requestConfetti, + checkGiftCode, + shareTonAddress, + saveTonAddress, + } = getActions(); const lang = useLang(); @@ -179,6 +186,20 @@ const ActionMessage: FC = ({ senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic, ]); + useEffect(() => { + if (!message.isOutgoing && message.content.action!.type === 'tonAddressRequest') { + shareTonAddress({ + requesterId: message.senderId!, + requestedAt: message.date * 1000, + }); + } else if (!message.isOutgoing && message.content.action!.type === 'tonAddressResponse') { + saveTonAddress({ + chatId: message.senderId!, + address: message.content.text!.text.replace(TON_MSG_ADDRESS_RESPONSE, ''), + }); + } + }, [message, saveTonAddress, shareTonAddress]); + const { isContextMenuOpen, contextMenuPosition, handleBeforeContextMenu, handleContextMenu, diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index d9f7bad6dc..8c8cd6202e 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -30,6 +30,8 @@ import AttachBotItem from './AttachBotItem'; import './AttachMenu.scss'; +import tonGemPath from '../../../assets/ton-gem.png'; + export type OwnProps = { chatId: string; threadId?: number; @@ -49,6 +51,8 @@ export type OwnProps = { onPollCreate: NoneToVoidFunction; onMenuOpen: NoneToVoidFunction; onMenuClose: NoneToVoidFunction; + canSendTons: boolean; + onSendTons: () => void; }; const AttachMenu: FC = ({ @@ -70,6 +74,8 @@ const AttachMenu: FC = ({ onMenuOpen, onMenuClose, onPollCreate, + canSendTons, + onSendTons, }) => { const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag(); const [handleMouseEnter, handleMouseLeave, markMouseInside] = useMouseInside(isAttachMenuOpen, closeAttachMenu); @@ -209,7 +215,6 @@ const AttachMenu: FC = ({ {canAttachPolls && ( {lang('Poll')} )} - {canAttachMedia && !isScheduled && bots?.map((bot) => ( = ({ onMenuClosed={unmarkAttachmentBotMenuOpen} /> ))} + {canSendTons && ( + } + onClick={onSendTons} + > + Send TON + + )} ); diff --git a/src/components/middle/composer/TonModal.async.tsx b/src/components/middle/composer/TonModal.async.tsx new file mode 100644 index 0000000000..5132e27854 --- /dev/null +++ b/src/components/middle/composer/TonModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './TonModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const TonModalAsync: FC = (props) => { + const { isOpen } = props; + const TonModal = useModuleLoader(Bundles.Extra, 'TonModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return TonModal ? : undefined; +}; + +export default TonModalAsync; diff --git a/src/components/middle/composer/TonModal.scss b/src/components/middle/composer/TonModal.scss new file mode 100644 index 0000000000..6b8dbeed95 --- /dev/null +++ b/src/components/middle/composer/TonModal.scss @@ -0,0 +1,83 @@ +.TonModal { + .modal-dialog { + max-width: 28rem; + + @media(max-width: 600px) { + max-height: 100%; + padding-bottom: 1.5rem; + } + } + + .address { + margin-bottom: 2rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .ChatInfo { + display: flex; + flex-direction: column; + align-items: center; + } + + .Avatar { + margin-bottom: 0.5rem; + } + + h3 { + margin: 0; + text-align: center; + } + + .pictogram { + margin: 0.5rem 0; + width: 6rem; + height: 6rem; + + .ton-pictogram { + width: 100%; + height: 100%; + } + } + + code { + font-size: 0.875rem; + color: var(--color-code); + background: var(--color-code-bg); + white-space: pre-wrap; + margin: 0; + padding: 1px 2px; + border-radius: 4px; + } + } + + .send-form { + .InputText { + margin: 0; + } + } + + .note { + margin-top: 0.5rem; + text-align: center; + font-size: 0.875rem; + color: var(--color-text-secondary); + + &.big { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 4rem 0 5rem 0; + } + + &.left { + text-align: left; + } + + .Spinner { + margin-bottom: 1rem; + } + } +} diff --git a/src/components/middle/composer/TonModal.tsx b/src/components/middle/composer/TonModal.tsx new file mode 100644 index 0000000000..ddc94f1b17 --- /dev/null +++ b/src/components/middle/composer/TonModal.tsx @@ -0,0 +1,229 @@ +import type { ChangeEvent } from 'react'; +import type { FC } from '../../../lib/teact/teact'; +import React, { + memo, useCallback, useEffect, useState, +} from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import { TON_MAGIC_URL } from '../../../config'; +import captureEscKeyListener from '../../../util/captureEscKeyListener'; +import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; + +import PrivateChatInfo from '../../common/PrivateChatInfo'; +import Button from '../../ui/Button'; +import InputText from '../../ui/InputText'; +import Modal from '../../ui/Modal'; +import Spinner from '../../ui/Spinner'; + +import './TonModal.scss'; + +export interface OwnProps { + isOpen: boolean; + chatId: string; + onClear: () => void; +} + +interface StateProps { + receiverAddress?: string; +} + +const DEFAULT_AMOUNT = 10; +const NANOTONS_IN_TON = 1e9; + +const TonModal: FC = ({ + isOpen, + chatId, + onClear, + receiverAddress, +}) => { + const { requestTonAddress, showNotification } = getActions(); + + const [amount, setAmount] = useState(DEFAULT_AMOUNT); + const [error, setError] = useState(); + + const { + isWalletInstalled, canInstallWallet, walletBalance, sendTons, + } = useTonWallet(isOpen); + + const handleAmountChange = useCallback((e: ChangeEvent) => { + setError(undefined); + + const value = e.currentTarget.value.replace(/[^\d.,]/, ''); + e.currentTarget.value = value; + + const numeric = value.replace(/,/g, '.').replace(/\.$/, ''); + setAmount(Number(numeric) || 0); + }, []); + + const canSend = receiverAddress && walletBalance && amount > 0 && walletBalance >= amount; + + const send = useCallback(() => { + setError(undefined); + + if (!canSend) { + return; + } + + // TODO Request throwing exceptions + const result = sendTons(receiverAddress!, amount); + if (result instanceof Promise) { + result + .then(() => { + showNotification({ message: 'TON successfully sent' }); + }) + .catch((err) => { + setError(err.message); + }); + } else if (result instanceof Error) { + setError(result.message); + } else { + setError('Unknown Error'); + } + }, [amount, canSend, receiverAddress, sendTons, showNotification]); + + useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]); + useEffect(() => (isOpen ? captureKeyboardListeners({ onEnter: send }) : undefined), [isOpen, send]); + + useEffect(() => { + if (!isOpen) { + return; + } + + if (!receiverAddress) { + requestTonAddress(); + } + }, [isOpen, receiverAddress, requestTonAddress]); + + function renderHeader() { + return ( +
+ +
Send TON
+ +
+ ); + } + + function renderContent() { + if (!isWalletInstalled && !canInstallWallet) { + return ( +
+ Sending TON is only supported in Chrome +
at this moment. +
+ ); + } + + return ( + <> + {receiverAddress ? ( +
+ +
+ ) : ( +
+ + Awaiting user to share his TON address... +
+ )} +
+ {isWalletInstalled ? ( + <> + +
+ Available balance: {walletBalance} TON +
+ + ) : ( + <> + +
+ You will need to refresh the page +
once extension is installed. +
+ + )} +
+ + ); + } + + return ( + + {renderContent()} + + ); +}; + +function useTonWallet(isOpen: boolean) { + const { ton, chrome } = window as any; + const [walletAddress, setWalletAddress] = useState(); + const [walletBalance, setWalletBalance] = useState(); + + const isWalletInstalled = ton && ton.isTonWallet; + + useEffect(() => { + if (!isWalletInstalled || !isOpen) { + return; + } + + // TODO Replace with real balance request + ton.send('ton_requestAccounts').then((accounts: string[]) => { + setWalletAddress(accounts[0]); + }); + ton.send('ton_getBalance').then((balance: number) => { + setWalletBalance(balance / NANOTONS_IN_TON); + }); + }, [isWalletInstalled, ton, isOpen]); + + const sendTons = useCallback((to: string, amount: number) => { + // TODO Make sure `walletAddress` exists + return ton.send('ton_sendTransaction', [{ + from: walletAddress, + value: String(amount * NANOTONS_IN_TON), + to, + data: 'Sent from Telegram WebZ', + }]); + }, [ton, walletAddress]); + + return { + canInstallWallet: Boolean(chrome.runtime), + isWalletInstalled, + walletBalance, + sendTons, + }; +} + +export default memo(withGlobal( + (global, { chatId }): StateProps => { + return { + receiverAddress: (global.ton.byChatId[chatId] || {}).address, + }; + }, +)(TonModal)); diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 87c929bfba..01b6f1137f 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -168,6 +168,8 @@ const Button: FC = ({ id={id} className={fullClassName} href={href} + target="_blank" + rel="noopener noreferrer" title={ariaLabel} download={download} tabIndex={tabIndex} diff --git a/src/components/ui/InputText.tsx b/src/components/ui/InputText.tsx index 8c924742f3..151c8f6cb9 100644 --- a/src/components/ui/InputText.tsx +++ b/src/components/ui/InputText.tsx @@ -58,7 +58,7 @@ const InputText: FC = ({ const lang = useLang(); const labelText = error || success || label; const fullClassName = buildClassName( - 'input-group', + 'InputText input-group', value && 'touched', error ? 'error' : success && 'success', disabled && 'disabled', diff --git a/src/config.ts b/src/config.ts index af1bbf12d2..f2f9e82a88 100644 --- a/src/config.ts +++ b/src/config.ts @@ -339,3 +339,9 @@ export const DEFAULT_LIMITS: Record = { chatlistJoined: [2, 20], recommendedChannels: [10, 100], }; + +// TON magic messages +export const TON_MAGIC_URL = 'https://telegra.ph/Telegram--TON-11-10'; +export const TON_MSG_ADDRESS_RESPONSE = 'My TON address is: '; +// eslint-disable-next-line max-len +export const TON_MSG_ADDRESS_REQUEST = `I want to send you TON. Please provide your wallet address in a reply message in this format:\n\n${TON_MSG_ADDRESS_RESPONSE}YOUR ADDRESS\n\nMore about TON + Telegram integration: ${TON_MAGIC_URL}`; diff --git a/src/global/actions/all.ts b/src/global/actions/all.ts index eaf9c5f6a3..0106cb60aa 100644 --- a/src/global/actions/all.ts +++ b/src/global/actions/all.ts @@ -15,6 +15,7 @@ import './api/payments'; import './api/reactions'; import './api/statistics'; import './api/stories'; +import './api/ton'; import './ui/initial'; import './ui/chats'; import './ui/messages'; diff --git a/src/global/actions/api/ton.ts b/src/global/actions/api/ton.ts new file mode 100644 index 0000000000..5c1d515e04 --- /dev/null +++ b/src/global/actions/api/ton.ts @@ -0,0 +1,101 @@ +import type { GlobalState } from '../../types'; + +import { TON_MSG_ADDRESS_REQUEST, TON_MSG_ADDRESS_RESPONSE } from '../../../config'; +import { getCurrentTabId } from '../../../util/establishMultitabRole'; +import { addActionHandler, getGlobal, setGlobal } from '../..'; +import { getMessageText } from '../../helpers'; +import { selectChatMessages, selectCurrentMessageList } from '../../selectors'; + +addActionHandler('requestTonAddress', (global, actions, payload): void => { + const { tabId = getCurrentTabId() } = payload || {}; + const { chatId } = selectCurrentMessageList(global, tabId) || {}; + if (!chatId) { + return; + } + + const wasRequested = Object.values(selectChatMessages(global, chatId)).some((message) => { + return message.isOutgoing && getMessageText(message) === TON_MSG_ADDRESS_REQUEST; + }); + if (wasRequested) { + return; + } + + const currentMessageList = selectCurrentMessageList(global, tabId); + actions.sendMessage({ + messageList: currentMessageList!, + text: TON_MSG_ADDRESS_REQUEST, + tabId, + }); +}); + +addActionHandler('shareTonAddress', (global, actions, payload): void => { + const { ton } = window as any; + if (!ton) { + return; + } + + const { requesterId, requestedAt, tabId = getCurrentTabId() } = payload; + + const { lastAddressShareAt } = global.ton.byChatId[requesterId] || {}; + if (lastAddressShareAt && lastAddressShareAt >= requestedAt) { + return; + } + + (async () => { + const addresses = await ton.send('ton_requestAccounts'); + + global = getGlobal(); + + const { chatId } = selectCurrentMessageList(global, tabId) || {}; + if (chatId !== requesterId) { + return; + } + + const currentMessageList = selectCurrentMessageList(global, tabId); + actions.sendMessage({ + messageList: currentMessageList!, + text: `${TON_MSG_ADDRESS_RESPONSE}${addresses[0]}`, + tabId, + }); + + global = { + ...global, + ton: { + ...global.ton, + byChatId: { + ...global.ton.byChatId, + [requesterId]: { + ...global.ton.byChatId[requesterId], + lastAddressShareAt: Date.now(), + }, + }, + }, + }; + + setGlobal(global); + })(); +}); + +addActionHandler('saveTonAddress', (global, actions, payload): GlobalState | void => { + const { chatId, address } = payload; + + const { address: currentAddress } = global.ton.byChatId[chatId] || {}; + + if (currentAddress === address) { + return undefined; + } + + return { + ...global, + ton: { + ...global.ton, + byChatId: { + ...global.ton.byChatId, + [chatId]: { + ...global.ton.byChatId[chatId], + address, + }, + }, + }, + }; +}); diff --git a/src/global/cache.ts b/src/global/cache.ts index ea0707007b..edac6f80c8 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -214,6 +214,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { untypedCached.appConfig.peerColors = undefined; untypedCached.appConfig.darkPeerColors = undefined; } + + if (!cached.ton) { + cached.ton = { byChatId: {} }; + } } function updateCache() { @@ -272,6 +276,7 @@ export function serializeGlobal(global: T) { 'trustedBotIds', 'recentlyFoundChatIds', 'peerColors', + 'ton', ]), lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined, customEmojis: reduceCustomEmojis(global), diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index f9c86f4096..28ce42acc4 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -173,7 +173,14 @@ export function isForwardedMessage(message: ApiMessage) { } export function isActionMessage(message: ApiMessage) { - return Boolean(message.content.action); + return ( + Boolean(message.content.action) + && !( + // We want to render normal message when TON wallet is not available, + // but we can not figure that out from within worker + message.content.action!.type === 'tonAddressRequest' && !message.isOutgoing && !(window as any).ton + ) + ); } export function isServiceNotificationMessage(message: ApiMessage) { diff --git a/src/global/initialState.ts b/src/global/initialState.ts index febc75b60f..4f1dee6381 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -266,6 +266,10 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { byChatId: {}, }, + ton: { + byChatId: {}, + }, + byTabId: {}, archiveSettings: { diff --git a/src/global/types.ts b/src/global/types.ts index ce8f6ed3f4..1dcc9d1fad 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -953,6 +953,13 @@ export type GlobalState = { serviceNotifications: ServiceNotification[]; + ton: { + byChatId: Record; + }; + byTabId: Record; archiveSettings: { @@ -967,7 +974,7 @@ export type GlobalState = { export type CallSound = ( 'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing' -); + ); export interface RequiredActionPayloads { apiUpdate: ApiUpdate; @@ -2884,6 +2891,17 @@ export interface ActionPayloads { file?: File; isSuggest?: boolean; } & WithTabId; + + // TON + requestTonAddress: WithTabId | undefined; + shareTonAddress: { + requesterId: string; + requestedAt: number; + } & WithTabId; + saveTonAddress: { + chatId: string; + address: string; + }; } export type RequiredGlobalState = GlobalState & { _: never }; diff --git a/src/styles/index.scss b/src/styles/index.scss index b7317472a3..23a1a5d0a8 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -385,3 +385,11 @@ body:not(.is-ios) { --color-forum-hover-unread-topic-hover: rgb(63, 63, 63); --color-chat-username: rgb(233, 238, 244); } + +.icon-custom-image { + width: 1.5rem; + height: 1.25rem; + background-repeat: no-repeat; + background-size: 100%; + background-position: center; +} diff --git a/webpack.config.ts b/webpack.config.ts index 840871a495..15c040fd7d 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -54,7 +54,13 @@ const CSP = ` export default function createConfig( _: any, - { mode = 'production' }: { mode: 'none' | 'development' | 'production' }, + { + mode = 'production', + 'output-path': outputPath, + }: { + mode: 'none' | 'development' | 'production'; + ['output-path']: string; + }, ): Configuration { return { mode, @@ -101,7 +107,7 @@ export default function createConfig( filename: '[name].[contenthash].js', chunkFilename: '[id].[chunkhash].js', assetModuleFilename: '[name].[contenthash][ext]', - path: path.resolve(__dirname, 'dist'), + path: path.resolve(__dirname, outputPath || 'dist'), clean: true, },