diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml deleted file mode 100644 index 48fa5a33347..00000000000 --- a/.github/workflows/desktop.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Desktop-release - -on: - push: - branches: - - desktop - -jobs: - release-mac: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [macos-11] - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Install Node.js, NPM and Yarn - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: npm install, lint and/or test - run: | - yarn - cd src/desktop - yarn - - - name: Build/release Electron app - uses: samuelmeuli/action-electron-builder@v1 - with: - # GitHub token, automatically provided to the action - # (No need to define this secret in the repo settings) - github_token: ${{ secrets.github_token }} - package_root: "./src/desktop/" - mac_certs: ${{ secrets.mac_certs }} - mac_certs_password: ${{ secrets.mac_certs_password }} - args: "--publish always" - max_attempts: "5" - env: - NODE_OPTIONS: '--max_old_space_size=4096' - - release-win: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [windows-latest] - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Install Node.js, NPM and Yarn - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: npm install, lint and/or test - run: | - yarn - cd src/desktop - yarn - - - name: Build/release Electron app - uses: samuelmeuli/action-electron-builder@v1 - with: - # GitHub token, automatically provided to the action - # (No need to define this secret in the repo settings) - github_token: ${{ secrets.github_token }} - package_root: "./src/desktop/" - windows_certs: ${{ secrets.windows_certs }} - windows_certs_password: ${{ secrets.windows_certs_password }} - args: "--publish always" - max_attempts: "5" - env: - NODE_OPTIONS: '--max_old_space_size=4096' - - release-unix: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - - - name: Install Node.js, NPM and Yarn - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: npm install, lint and/or test - run: | - yarn - cd src/desktop - yarn - - - name: Build/release Electron app - uses: samuelmeuli/action-electron-builder@v1 - with: - # GitHub token, automatically provided to the action - # (No need to define this secret in the repo settings) - github_token: ${{ secrets.github_token }} - package_root: "./src/desktop/" - args: "--publish always" - max_attempts: "5" - env: - NODE_OPTIONS: '--max_old_space_size=4096' diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7833b2e43bf..ad1bc0e1dff 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -18,9 +18,6 @@ jobs: - name: npm install, lint and/or test run: | yarn - cd src/desktop - yarn - cd ../../ yarn test --runInBand --updateSnapshot env: CI: true diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 24c3d56e00f..960f745b375 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -18,9 +18,6 @@ jobs: - name: npm install, lint and/or test run: | yarn - cd src/desktop - yarn - cd ../../ yarn test --runInBand --updateSnapshot env: CI: true diff --git a/Dockerfile b/Dockerfile index 4bb3a34f2cb..db66247a639 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,10 @@ -FROM node:16.13.2 as base +FROM node:16.15.1 as base WORKDIR /var/app COPY package.json yarn.lock ./ -COPY src/desktop/package.json src/desktop/yarn.lock ./src/desktop/ RUN yarn install --non-interactive --frozen-lockfile --ignore-optional -RUN yarn install --non-interactive --frozen-lockfile --ignore-optional --cwd src/desktop COPY . . @@ -18,7 +16,7 @@ FROM base as dependencies RUN yarn install --non-interactive --frozen-lockfile --ignore-optional --production ### BUILD MINIFIED PRODUCTION ## -FROM node:16.13.2 as production +FROM node:16.15.1 as production # Add Tini ENV TINI_VERSION v0.18.0 diff --git a/README.md b/README.md index 4adf6cc61ab..9613330b2c6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Discord](https://img.shields.io/discord/385034494555455488?label=Ecency%20discord&logo=discord) ![Twitter Follow](https://img.shields.io/twitter/follow/ecency_official?style=social) ![GitHub Repo stars](https://img.shields.io/github/stars/ecency/ecency-vision?style=social) -# [Ecency vision][ecency_vision] – Ecency Web/Desktop client +# [Ecency vision][ecency_vision] – Ecency Web client ![ecency](https://ecency.com/assets/github-cover.png) @@ -13,14 +13,6 @@ Fast, simple and clean source code with Reactjs + Typescript. - [Production version][ecency_vision] - master branch - [Alpha version][ecency_alpha] - development branch -## Desktop app - -Please check latest version on [Release page][ecency_release] or [Ecency link][ecency_desktop]. - -- Mac users: `Ecency-3.x.x.dmg` -- Windows users: `Ecency.Setup.3.x.x.exe` -- Linux users: `ecency-surfer_3.x.x_amd_64.deb`, `Ecency-3.x.x.AppImage`, `ecency-surfer-3.x.x.x86_64.rpm`, `ecency-surfer-3.x.x.tar.gz` - ## Developers Feel free to test it out and submit improvements and pull requests. @@ -73,12 +65,6 @@ If you are setting up your own website other than Ecency.com, you can still leav `$ yarn start` -##### Start desktop in dev - -`$ cd src/desktop` -`$ yarn` -`$ yarn dev` - ##### Pushing new code / Pull requests - Make sure to branch off your changes from `development` branch. @@ -133,6 +119,5 @@ We will evaluate the risk and make a patch available before filing the issue. [//]: # "LINKS" [ecency_vision]: https://ecency.com -[ecency_desktop]: https://desktop.ecency.com [ecency_alpha]: https://alpha.ecency.com [ecency_release]: https://github.com/ecency/ecency-vision/releases diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 0108d1b86b5..9ddb1034c00 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -12,7 +12,7 @@ services: command: - /bin/sh - -c - - redis-server --save 20 1 --loglevel warning --requirepass "$${REDIS_HOST_PASSWORD:?REDIS_HOST_PASSWORD variable is not set}" + - redis-server --save 20 1 --appendonly no --maxmemory 16gb --maxmemory-policy allkeys-lru --loglevel warning --requirepass "$${REDIS_HOST_PASSWORD:?REDIS_HOST_PASSWORD variable is not set}" volumes: - redis:/data networks: @@ -109,4 +109,4 @@ volumes: networks: vision: - external: true \ No newline at end of file + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 9ef5262f80b..0e6b8aa8873 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: command: - /bin/sh - -c - - redis-server --save 20 1 --loglevel warning --requirepass "$${REDIS_HOST_PASSWORD:?REDIS_HOST_PASSWORD variable is not set}" + - redis-server --save 20 1 --appendonly no --maxmemory 16gb --maxmemory-policy allkeys-lru --loglevel warning --requirepass "$${REDIS_HOST_PASSWORD:?REDIS_HOST_PASSWORD variable is not set}" volumes: - redis:/data networks: @@ -109,4 +109,4 @@ volumes: networks: vision: - external: true \ No newline at end of file + external: true diff --git a/package.json b/package.json index 2bdd8f596fd..7b84694e740 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecency-vision", - "version": "3.0.36", + "version": "3.2.0", "private": true, "license": "MIT", "scripts": { @@ -13,7 +13,8 @@ "start:prod": "NODE_ENV=production node build/server.js" }, "dependencies": { - "@ecency/render-helper": "^2.2.26", + "@ecency/ns-query": "^1.2.5", + "@ecency/render-helper": "^2.2.32", "@ecency/render-helper-amp": "^1.1.0", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", @@ -22,8 +23,10 @@ "@firebase/messaging": "^0.9.16", "@hiveio/dhive": "^1.3.0", "@hiveio/hivescript": "^1.2.7", + "@iconscout/react-unicons": "^2.0.2", "@loadable/component": "^5.15.2", "@loadable/server": "^5.15.2", + "@noble/secp256k1": "^1.7.1", "@popperjs/core": "^2.11.8", "@tanstack/react-query": "^4.29.7", "@tanstack/react-query-devtools": "^4.29.7", @@ -35,6 +38,7 @@ "connected-react-router": "^6.8.0", "cookie-parser": "^1.4.5", "currency-symbol-map": "^4.0.4", + "date-fns": "^2.30.0", "debounce": "^1.2.1", "diff-match-patch": "^1.0.5", "emoji-mart": "^5.5.2", @@ -55,6 +59,7 @@ "moment": "^2.29.4", "node-cache": "^5.1.0", "node-html-parser": "^5.3.3", + "nostr-relaypool": "^0.6.28", "numeral": "^2.0.6", "path-to-regexp": "^6.1.0", "qrcode": "^1.5.1", @@ -71,6 +76,7 @@ "react-highcharts": "^16.1.0", "react-img-webp": "^2.0.2", "react-in-viewport": "^1.0.0-alpha.30", + "react-input-slider": "^6.0.1", "react-popper": "^2.2.5", "react-redux": "^7.2.0", "react-resize-detector": "^7.1.2", @@ -89,7 +95,7 @@ "sortablejs": "^1.13.0", "speakingurl": "^14.0.1", "tough-cookie": "^4.1.2", - "tus-js-client": "^3.1.0", + "tus-js-client": "3.1.0", "use-async-effect": "^2.2.6", "use-indexeddb": "^2.0.2", "uuid": "^9.0.0", @@ -97,6 +103,7 @@ }, "devDependencies": { "@loadable/webpack-plugin": "^5.15.2", + "@testing-library/jest-dom": "^6.1.6", "@thoughtbot/tailwindcss-aria-attributes": "^0.2.0", "@types/bs58": "^4.0.1", "@types/bytebuffer": "^5.0.41", @@ -135,19 +142,19 @@ "@types/webpack-env": "^1.14.0", "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", "autoprefixer": "^10.4.14", - "babel-preset-razzle": "^4.0.5", + "babel-preset-razzle": "^4.2.18", "html-webpack-plugin": "4.5.2", "husky": "^8.0.1", "jest": "^26.0.0", "lint-staged": "^13.0.3", "mini-css-extract-plugin": "0.9.0", "mockdate": "^3.0.2", - "postcss": "8.2.13", + "postcss": "8.4.31", "postcss-cli": "^10.1.0", "prettier": "^2.7.1", - "razzle": "^4.0.5", - "razzle-dev-utils": "^4.0.5", - "razzle-plugin-scss": "^4.0.5", + "razzle": "^4.2.18", + "razzle-dev-utils": "^4.2.18", + "razzle-plugin-scss": "^4.2.18", "razzle-plugin-typescript": "^3.0.0", "react-test-renderer": "^16.13.1", "tailwindcss": "^3.3.2", @@ -171,6 +178,9 @@ ] }, "jest": { + "setupFilesAfterEnv": [ + "/src/test-setup.ts" + ], "transform": { "\\.(ts|tsx)$": "ts-jest", "\\.css$": "/node_modules/razzle/config/jest/cssTransform.js", diff --git a/src/common/api/hive-engine.ts b/src/common/api/hive-engine.ts index 3d367a337b0..859ecda0fc5 100644 --- a/src/common/api/hive-engine.ts +++ b/src/common/api/hive-engine.ts @@ -3,6 +3,7 @@ import HiveEngineToken from "../helper/hive-engine-wallet"; import { TransactionConfirmation } from "@hiveio/dhive"; import { broadcastPostingJSON } from "./operations"; import engine from "../constants/engine.json"; +import { apiBase } from "./helper"; interface TokenBalance { symbol: string; @@ -43,8 +44,6 @@ export interface TokenStatus { precision: number; } -const HIVE_ENGINE_RPC_URL = engine.engineRpcUrl; - export const getTokenBalances = (account: string): Promise => { const data = { jsonrpc: "2.0", @@ -60,7 +59,7 @@ export const getTokenBalances = (account: string): Promise => { }; return axios - .post(HIVE_ENGINE_RPC_URL, data, { + .post(apiBase(engine.API), data, { headers: { "Content-type": "application/json" } }) .then((r) => r.data.result) @@ -84,7 +83,7 @@ const getTokens = (tokens: string[]): Promise => { }; return axios - .post(HIVE_ENGINE_RPC_URL, data, { + .post(apiBase(engine.API), data, { headers: { "Content-type": "application/json" } }) .then((r) => r.data.result) @@ -108,10 +107,9 @@ export const getHiveEngineTokenBalances = async (account: string): Promise => { - const rewardsUrl = engine.engineRewardsUrl; return ( axios - .get(`${rewardsUrl}/@${account}?hive=1`) + .get(apiBase(`${engine.rewardAPI}/${account}?hive=1`)) .then((r) => r.data) .then((r) => Object.values(r)) .then((r) => r.filter((t) => (t as TokenStatus).pending_token > 0)) as any @@ -172,7 +170,7 @@ export const getMetrics: any = async (symbol?: any, account?: any) => { // }) // return result; return axios - .post(HIVE_ENGINE_RPC_URL, data, { + .post(apiBase(engine.API), data, { headers: { "Content-type": "application/json" } }) .then((r) => r.data.result) @@ -182,8 +180,7 @@ export const getMetrics: any = async (symbol?: any, account?: any) => { }; export const getMarketData = async (symbol: any) => { - const url: any = engine.chartApi; - const { data: history } = await axios.get(`${url}`, { + const { data: history } = await axios.get(apiBase(`${engine.chartAPI}`), { params: { symbol, interval: "daily" } }); return history; diff --git a/src/common/api/hive.ts b/src/common/api/hive.ts index 25e6fa01b21..a520bc93310 100644 --- a/src/common/api/hive.ts +++ b/src/common/api/hive.ts @@ -1,4 +1,4 @@ -import { Client, RCAPI, utils } from "@hiveio/dhive"; +import { Client, RCAPI, SMTAsset, utils } from "@hiveio/dhive"; import { RCAccount } from "@hiveio/dhive/lib/chain/rc"; @@ -286,8 +286,11 @@ export const getAccounts = (usernames: string[]): Promise => { ); }; -export const getAccount = (username: string): Promise => - getAccounts([username]).then((resp) => resp[0]); +export const getAccount = async (username: string): Promise => { + let aa = await getAccounts([username]).then((resp) => resp[0]); + let rp = await getAccountReputations(username, 1); + return { ...aa, ...rp[0] }; +}; export const getAccountFull = (username: string): Promise => getAccount(username).then(async (account) => { @@ -447,11 +450,7 @@ export const getWitnessesByVote = (from: string, limit: number): Promise => client .call("database_api", "list_proposals", { start: [-1], - limit: 200, + limit: 500, order: "by_total_votes", order_direction: "descending", status: "all" }) .then((r) => r.proposals); +export const findProposals = (id: number): Promise => + client.call("condenser_api", "find_proposals", [[id]]).then((r) => r[0]); + export interface ProposalVote { id: number; proposal: Proposal; diff --git a/src/common/api/misc.ts b/src/common/api/misc.ts index 5b13768e72d..5aca396608d 100644 --- a/src/common/api/misc.ts +++ b/src/common/api/misc.ts @@ -45,15 +45,10 @@ export const getCurrencyRate = (cur: string): Promise => { return axios .get(u) .then((r) => r.data) - .then((r) => r.hive_dollar[cur]); + .then((r) => r.hive_dollar[cur]) + .catch((e) => {}); }; -export const geLatestDesktopTag = (): Promise => - axios - .get("https://api.github.com/repos/ecency/ecency-vision/releases/latest") - .then((r) => r.data) - .then((r) => r.tag_name); - export const GIPHY_API_KEY = "DQ7mV4VsZ749GcCBZEunztICJ5nA4Vef"; export const GIPHY_API = `https://api.giphy.com/v1/gifs/trending?api_key=${GIPHY_API_KEY}&limit=10&offset=0`; export const GIPHY_SEARCH_API = `https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&limit=40&offset=0&q=`; diff --git a/src/common/api/mutations.ts b/src/common/api/mutations/account-claiming.ts similarity index 56% rename from src/common/api/mutations.ts rename to src/common/api/mutations/account-claiming.ts index 5733eb948a1..6422a564317 100644 --- a/src/common/api/mutations.ts +++ b/src/common/api/mutations/account-claiming.ts @@ -1,21 +1,7 @@ +import { FullAccount } from "../../store/accounts/types"; import { useMutation } from "@tanstack/react-query"; -import { usrActivity } from "./private-api"; -import { claimAccount, claimAccountByKeychain } from "./operations"; -import { FullAccount } from "../store/accounts/types"; import { PrivateKey } from "@hiveio/dhive"; - -interface Params { - bl?: string | number; - tx?: string | number; -} - -export function useUserActivity(username: string | undefined, ty: number) { - return useMutation(["user-activity", username, ty], async (params: Params | undefined) => { - if (username) { - await usrActivity(username, ty, params?.bl, params?.tx); - } - }); -} +import { claimAccount, claimAccountByKeychain } from "../operations"; export function useAccountClaiming(account: FullAccount) { return useMutation( diff --git a/src/common/api/mutations/create-reply.ts b/src/common/api/mutations/create-reply.ts new file mode 100644 index 00000000000..dadc3dd09e6 --- /dev/null +++ b/src/common/api/mutations/create-reply.ts @@ -0,0 +1,94 @@ +import { Entry } from "../../store/entries/types"; +import { useMappedStore } from "../../store/use-mapped-store"; +import { useContext } from "react"; +import { EntriesCacheContext, QueryIdentifiers } from "../../core"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { comment, CommentOptions, formatError, MetaData } from "../operations"; +import tempEntry from "../../helper/temp-entry"; +import { FullAccount } from "../../store/accounts/types"; +import * as ss from "../../util/session-storage"; +import { error } from "../../components/feedback"; + +export function useCreateReply(entry: Entry | null, parent?: Entry, onSuccess?: () => void) { + const { activeUser } = useMappedStore(); + const { addReply, updateRepliesCount, updateCache } = useContext(EntriesCacheContext); + const queryClient = useQueryClient(); + + return useMutation( + ["reply-create", activeUser?.username, entry?.author, entry?.permlink], + async ({ + permlink, + text, + jsonMeta, + options, + point + }: { + permlink: string; + text: string; + jsonMeta: MetaData; + point: boolean; + options?: CommentOptions; + }) => { + if (!activeUser || !activeUser.data.__loaded || !entry) { + throw new Error("[Reply][Create] – no active user provided"); + } + + await comment( + activeUser.username, + entry.author, + entry.permlink, + permlink, + "", + text, + jsonMeta, + options ?? null, + point + ); + return tempEntry({ + author: activeUser.data as FullAccount, + permlink, + parentAuthor: entry.author, + parentPermlink: entry.permlink, + title: "", + body: text, + tags: [], + description: null + }); + }, + { + onSuccess: (data) => { + if (!entry) { + return; + } + + addReply(entry, data); + updateCache([data]); + + // remove reply draft + ss.remove(`reply_draft_${entry.author}_${entry.permlink}`); + + if (entry.children === 0) { + // Update parent comment. + updateRepliesCount(entry, 1); + } + const previousReplies = + queryClient.getQueryData([ + QueryIdentifiers.FETCH_DISCUSSIONS, + parent?.author ?? entry.author, + parent?.permlink ?? entry.permlink + ]) ?? []; + queryClient.setQueryData( + [ + QueryIdentifiers.FETCH_DISCUSSIONS, + parent?.author ?? entry.author, + parent?.permlink ?? entry.permlink + ], + [data, ...previousReplies] + ); + + onSuccess?.(); + }, + onError: (e) => error(...formatError(e)) + } + ); +} diff --git a/src/common/api/mutations/index.ts b/src/common/api/mutations/index.ts new file mode 100644 index 00000000000..b7ded518e31 --- /dev/null +++ b/src/common/api/mutations/index.ts @@ -0,0 +1,5 @@ +export * from "./account-claiming"; +export * from "./create-reply"; +export * from "./update-reply"; +export * from "./user-activity"; +export * from "./pin-reply"; diff --git a/src/common/api/mutations/pin-reply.ts b/src/common/api/mutations/pin-reply.ts new file mode 100644 index 00000000000..775034b0b0c --- /dev/null +++ b/src/common/api/mutations/pin-reply.ts @@ -0,0 +1,22 @@ +import { useMutation } from "@tanstack/react-query"; +import { Entry } from "../../store/entries/types"; +import { useUpdateReply } from "./update-reply"; +import { EntryBodyManagement, EntryMetadataManagement } from "../../features/entry-management"; + +export function usePinReply(reply: Entry, parent: Entry) { + const { mutateAsync: updateReply } = useUpdateReply(parent); + + return useMutation(["reply-pin", reply, parent], async ({ pin }: { pin: boolean }) => { + return updateReply({ + text: EntryBodyManagement.EntryBodyManager.shared + .builder() + .buildPatchFrom(parent, parent.body), + point: true, + jsonMeta: EntryMetadataManagement.EntryMetadataManager.shared + .builder() + .extend(parent) + .withPinnedReply(reply, pin) + .build() + }); + }); +} diff --git a/src/common/api/mutations/update-reply.ts b/src/common/api/mutations/update-reply.ts new file mode 100644 index 00000000000..a5d68086eb6 --- /dev/null +++ b/src/common/api/mutations/update-reply.ts @@ -0,0 +1,63 @@ +import { Entry } from "../../store/entries/types"; +import { useMappedStore } from "../../store/use-mapped-store"; +import { useContext } from "react"; +import { EntriesCacheContext } from "../../core"; +import { useMutation } from "@tanstack/react-query"; +import { comment, CommentOptions, formatError, MetaData } from "../operations"; +import * as ss from "../../util/session-storage"; +import { error } from "../../components/feedback"; + +export function useUpdateReply(entry: Entry | null, onSuccess?: () => void) { + const { activeUser } = useMappedStore(); + const { updateCache } = useContext(EntriesCacheContext); + + return useMutation( + ["reply-update", activeUser?.username, entry?.author, entry?.permlink], + async ({ + text, + jsonMeta, + options, + point + }: { + text: string; + jsonMeta: MetaData; + point: boolean; + options?: CommentOptions; + }) => { + if (!activeUser || !activeUser.data.__loaded || !entry) { + throw new Error("[Reply][Create] – no active user provided"); + } + + await comment( + activeUser.username, + entry.parent_author ?? "", + entry.parent_permlink ?? entry.category, + entry.permlink, + "", + text, + jsonMeta, + options ?? null, + point + ); + return { + ...entry, + json_metadata: jsonMeta, + body: text + }; + }, + { + onSuccess: (data) => { + if (!entry) { + return; + } + + updateCache([data]); + + // remove reply draft + ss.remove(`reply_draft_${entry.author}_${entry.permlink}`); + onSuccess?.(); + }, + onError: (e) => error(...formatError(e)) + } + ); +} diff --git a/src/common/api/mutations/user-activity.ts b/src/common/api/mutations/user-activity.ts new file mode 100644 index 00000000000..6e47ea33347 --- /dev/null +++ b/src/common/api/mutations/user-activity.ts @@ -0,0 +1,15 @@ +import { useMutation } from "@tanstack/react-query"; +import { usrActivity } from "../private-api"; + +interface Params { + bl?: string | number; + tx?: string | number; +} + +export function useUserActivity(username: string | undefined, ty: number) { + return useMutation(["user-activity", username, ty], async (params: Params | undefined) => { + if (username) { + await usrActivity(username, ty, params?.bl, params?.tx); + } + }); +} diff --git a/src/common/api/notifications-ws-api.ts b/src/common/api/notifications-ws-api.ts index 81bf1771383..5ee84055696 100644 --- a/src/common/api/notifications-ws-api.ts +++ b/src/common/api/notifications-ws-api.ts @@ -12,7 +12,6 @@ declare var window: Window & { export class NotificationsWebSocket { private activeUser: ActiveUser | null = null; - private isElectron = false; private hasNotifications = false; private hasUiNotifications = false; private onSuccessCallbacks: Function[] = []; @@ -55,11 +54,11 @@ export class NotificationsWebSocket { const permission = await requestNotificationPermission(); if (permission !== "granted") return; - playNotificationSound(this.isElectron); + playNotificationSound(); } private async onMessageReceive(evt: MessageEvent) { - const logo = this.isElectron ? "./img/logo-circle.svg" : require("../img/logo-circle.svg"); + const logo = require("../img/logo-circle.svg"); const data = JSON.parse(evt.data); const msg = NotificationsWebSocket.getBody(data); @@ -106,18 +105,18 @@ export class NotificationsWebSocket { window.nws = new WebSocket(`${defaults.nwsServer}/ws?user=${this.activeUser.username}`); window.nws.onopen = () => { - console.log("nws connected"); + console.debug("nws connected"); this.isConnected = true; }; window.nws.onmessage = (e) => this.onMessageReceive(e); window.nws.onclose = (evt: CloseEvent) => { - console.log("nws disconnected"); + console.debug("nws disconnected"); window.nws = undefined; if (!evt.wasClean) { // Disconnected due connection error - console.log("nws trying to reconnect"); + console.debug("nws trying to reconnect"); setTimeout(() => { this.connect(); @@ -139,11 +138,6 @@ export class NotificationsWebSocket { return this; } - public withElectron(isElectron: boolean) { - this.isElectron = isElectron; - return this; - } - public withToggleUi(toggle: Function) { this.toggleUiProp = toggle; return this; diff --git a/src/common/api/operations.ts b/src/common/api/operations.ts index ffb93c5d002..c21b969554c 100644 --- a/src/common/api/operations.ts +++ b/src/common/api/operations.ts @@ -38,9 +38,10 @@ export interface MetaData { app?: string; format?: string; community?: string; - description?: string; + description?: string | null; video?: any; type?: string; + pinned_reply?: string; // author/permlink } export interface BeneficiaryRoute { @@ -1206,6 +1207,21 @@ export const promote = ( return hiveClient.broadcast.json(op, key); }; +export const boostPlus = (key: PrivateKey, user: string, account: string, duration: number) => + hiveClient.broadcast.json( + { + id: "ecency_boost_plus", + json: JSON.stringify({ + user, + account, + duration + }), + required_auths: [user], + required_posting_auths: [] + }, + key + ); + export const promoteHot = (user: string, author: string, permlink: string, duration: number) => { const params = { authority: "active", @@ -1223,6 +1239,22 @@ export const promoteHot = (user: string, author: string, permlink: string, durat hotSign("custom-json", params, `@${user}/points`); }; +export const boostPlusHot = (user: string, account: string, duration: number) => { + const params = { + authority: "active", + required_auths: `["${user}"]`, + required_posting_auths: "[]", + id: "ecency_boost_plus", + json: JSON.stringify({ + user, + account, + duration + }) + }; + + hotSign("custom-json", params, `@${user}/points`); +}; + export const promoteKc = (user: string, author: string, permlink: string, duration: number) => { const json = JSON.stringify({ user, @@ -1234,6 +1266,16 @@ export const promoteKc = (user: string, author: string, permlink: string, durati return keychain.customJson(user, "ecency_promote", "Active", json, "Promote"); }; +export const boostPlusKc = (user: string, account: string, duration: number) => { + const json = JSON.stringify({ + user, + account, + duration + }); + + return keychain.customJson(user, "ecency_boost_plus", "Active", json, "Boost Plus"); +}; + export const boost = ( key: PrivateKey, user: string, diff --git a/src/common/api/private-api.ts b/src/common/api/private-api.ts index b109a4a354a..c9c4c8fb883 100644 --- a/src/common/api/private-api.ts +++ b/src/common/api/private-api.ts @@ -12,10 +12,10 @@ import { getAccessToken } from "../helper/user-token"; import { apiBase } from "./helper"; import { AppWindow } from "../../client/window"; -import isElectron from "../util/is-electron"; import { NotifyTypes } from "../enums"; import { BeneficiaryRoute, MetaData, RewardType } from "./operations"; import { ThreeSpeakVideo } from "./threespeak"; +import { PollSnapshot } from "../features/polls"; declare var window: AppWindow; @@ -218,6 +218,7 @@ export interface DraftMetadata extends MetaData { beneficiaries: BeneficiaryRoute[]; rewardType: RewardType; videos?: Record; + poll?: PollSnapshot; } export interface Draft { @@ -465,6 +466,22 @@ export const getPromotePrice = (username: string): Promise => { return axios.post(apiBase(`/private-api/promote-price`), data).then((resp) => resp.data); }; +export const getBoostPlusPrice = (username: string): Promise => { + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/boost-plus-price`), data).then((resp) => resp.data); +}; + +export const getBoostPlusAccounts = ( + username: string, + account: string +): Promise<{ + expires: string; + account: string; +}> => { + const data = { code: getAccessToken(username), account }; + return axios.post(apiBase("/private-api/boosted-plus-account"), data).then((resp) => resp.data); +}; + export const getPromotedPost = ( username: string, author: string, @@ -528,7 +545,7 @@ export const saveNotificationsSettings = ( ) => { return saveNotificationSetting( username, - isElectron() ? "desktop" : "web", + "web", Number(isEnabled), notifyTypes as number[], token @@ -602,6 +619,7 @@ export const getAnnouncementsData = async (): Promise => { throw error; } }; + export interface Recoveries { username: string; email: string; diff --git a/src/common/api/queries.ts b/src/common/api/queries.ts index 0e87bd38c74..ce250c05864 100644 --- a/src/common/api/queries.ts +++ b/src/common/api/queries.ts @@ -1,10 +1,22 @@ -import { useQuery } from "@tanstack/react-query"; -import { QueryIdentifiers } from "../core"; -import { getPoints, getPointTransactions } from "./private-api"; +import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { EntriesCacheContext, QueryIdentifiers } from "../core"; +import { + getBoostPlusAccounts, + getBoostPlusPrice, + getPoints, + getPointTransactions +} from "./private-api"; import { useMappedStore } from "../store/use-mapped-store"; import axios from "axios"; import { catchPostImage } from "@ecency/render-helper"; import { Entry } from "../store/entries/types"; +import { getAccountFull, getFollowing } from "./hive"; +import { getAccountPosts, getDiscussion } from "./bridge"; +import { SortOrder } from "../store/discussion/types"; +import { useContext, useState } from "react"; +import { sortDiscussions } from "../util/sort-discussions"; +import { apiBase } from "./helper"; +import useDebounce from "react-use/lib/useDebounce"; const DEFAULT = { points: "0.000", @@ -88,3 +100,119 @@ export function useImageDownloader( } ); } + +export function useGetAccountFullQuery(username?: string) { + return useQuery([QueryIdentifiers.GET_ACCOUNT_FULL, username], () => getAccountFull(username!), { + enabled: !!username + }); +} + +export function useGetAccountsFullQuery(usernames: string[]) { + return useQueries({ + queries: usernames.map((username) => ({ + queryKey: [QueryIdentifiers.GET_ACCOUNT_FULL, username], + queryFn: () => getAccountFull(username!), + enabled: !!username + })) + }); +} + +export function useGetAccountPostsQuery(username?: string) { + return useQuery({ + queryKey: [QueryIdentifiers.GET_POSTS, username], + queryFn: () => getAccountPosts("posts", username!).then((response) => response ?? []), + enabled: !!username, + initialData: [] + }); +} + +export function useFetchDiscussionsQuery( + entry: Entry, + order: SortOrder, + queryOptions?: UseQueryOptions +) { + const { updateCache } = useContext(EntriesCacheContext); + + return useQuery( + [QueryIdentifiers.FETCH_DISCUSSIONS, entry?.author, entry?.permlink], + async () => { + const response = await getDiscussion(entry.author, entry.permlink); + if (response) { + const entries = Array.from(Object.values(response)); + updateCache([...entries], true); + return entries; + } + return []; + }, + { + ...queryOptions, + initialData: [], + select: (data) => sortDiscussions(entry, data, order) + } + ); +} + +export function useFetchMutedUsersQuery(username?: string) { + const { activeUser } = useMappedStore(); + + return useQuery( + [QueryIdentifiers.FETCH_MUTED_USERS, username ?? activeUser?.username ?? "anon"], + async () => { + const response = await getFollowing(username ?? activeUser!!.username, "", "ignore", 100); + return response.map((user) => user.following); + }, + { + initialData: [], + enabled: !!username || !!activeUser + } + ); +} + +export function useBotsQuery() { + return useQuery({ + queryKey: [QueryIdentifiers.GET_BOTS], + queryFn: () => + axios.get(apiBase("/private-api/public/bots")).then((resp) => resp.data), + initialData: [] + }); +} + +export function useGetBoostPlusPricesQuery() { + const { activeUser } = useMappedStore(); + return useQuery({ + queryKey: [QueryIdentifiers.GET_BOOST_PLUS_PRICES], + queryFn: () => getBoostPlusPrice(activeUser!.username), + initialData: [] + }); +} + +export function useGetBoostPlusAccountPricesQuery(account: string) { + const { activeUser } = useMappedStore(); + + const [query, setQuery] = useState(""); + + useDebounce( + () => { + if (account) { + setQuery(account); + } + }, + 300, + [account] + ); + + return useQuery({ + queryKey: [QueryIdentifiers.GET_BOOST_PLUS_ACCOUNTS, query], + queryFn: () => + getBoostPlusAccounts(activeUser!.username, query).then((data) => + data + ? ({ + ...data, + expires: new Date(data.expires) + } as { account?: string; expires?: Date }) + : {} + ), + initialData: {}, + enabled: !!query + }); +} diff --git a/src/common/app.tsx b/src/common/app.tsx index 308da7ba8e7..7fb8b38dbf0 100644 --- a/src/common/app.tsx +++ b/src/common/app.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useMemo, useState } from "react"; import { Route, Switch } from "react-router-dom"; import EntryIndexContainer from "./pages/index"; import { EntryScreen } from "./pages/entry"; @@ -26,8 +26,16 @@ import Announcement from "./components/announcement"; import FloatingFAQ from "./components/floating-faq"; import { useMappedStore } from "./store/use-mapped-store"; import { EntriesCacheManager } from "./core"; - import { UserActivityRecorder } from "./components/user-activity-recorder"; +import { useGlobalLoader } from "./util/use-global-loader"; +import useMount from "react-use/lib/useMount"; +import { ChatPopUp } from "./features/chats/components/chat-popup"; +import { ChatContextProvider } from "@ecency/ns-query"; +import { useGetAccountFullQuery } from "./api/queries"; +import { UIManager } from "@ui/core"; +import defaults from "./constants/defaults.json"; +import { getAccessToken } from "./helper/user-token"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // Define lazy pages const ProfileContainer = loadable(() => import("./pages/profile-functional")); @@ -48,6 +56,9 @@ const AuthPage = (props: any) => ; const SubmitContainer = loadable(() => import("./pages/submit")); const SubmitPage = (props: any) => ; +const ChatsContainer = loadable(() => import("./features/chats/screens/chats")); +const ChatsPage = (props: any) => ; + const OnboardContainer = loadable(() => import("./pages/onboard")); const OnboardPage = (props: any) => ; @@ -75,12 +86,19 @@ const EntryPage = (props: any) => { const PurchaseContainer = loadable(() => import("./pages/purchase")); const PurchasePage = (props: any) => ; -const DecksPage = loadable(() => import("./pages/decks")); +const DecksPage = loadable(() => import("./features/decks/screens/decks")); +const EcencyPerksPage = loadable(() => import("./features/perks/screens/main")); const App = (props: any) => { - const { global } = useMappedStore(); + const { global, activeUser } = useMappedStore(); + const { hide } = useGlobalLoader(); + + const { data: activeUserAccount } = useGetAccountFullQuery(activeUser?.username); + + useMount(() => { + // Drop hiding from main queue to give React time to render + setTimeout(() => hide(), 1); - useEffect(() => { let pathname = window.location.pathname; if (pathname !== "/faq") { const currentLang = ls.get("current-language"); @@ -89,94 +107,142 @@ const App = (props: any) => { i18n.changeLanguage(currentLang); } } - }, []); + }); + + const accessToken = useMemo( + () => (activeUser ? getAccessToken(activeUser.username) ?? "" : ""), + [activeUser] + ); return ( - - {/*Excluded from production*/} - {/**/} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-