diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d87df284..5348b4ac 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ }, "customizations": { "vscode": { - "extensions": [] + "extensions": ["ms-playwright.playwright"] } }, "postCreateCommand": ".devcontainer/post-create.sh" diff --git a/docs/developing/dontaskagain.md b/docs/developing/dontaskagain.md new file mode 100644 index 00000000..241f161e --- /dev/null +++ b/docs/developing/dontaskagain.md @@ -0,0 +1,14 @@ +Developing with the "Don't ask again" feature +============================================= + +NEAR BOS has the "Don't ask again" feature on transactions for changes to SocialDB, or calling any contract. For an optimal user experience in BOS applications, both the transaction confirmation dialog, and transactions being executed without confirmation ( when the "Don't ask again" feature is enabled ), should be handled. + +When executing a transaction that does not confirm with the wallet, it is important to make the user aware of the ongoing transaction. NearSocialVM will show a toast in the bottom right, informing that a transaction is in process, but it might take time for this to appear, and also it does disappear when the transaction is complete, but still the page might not have been fully refreshed. + +We would like to avoid that the user double clicks any submit or like button, so then we should ensure that the button is disabled immediately once cliked. Also there should be an loading indicator on the button, like a spinner. + +After the transaction is complete, we would not want the user to think that a page reload is needed, so we still have to keep the loading indicator spinning, until VM cache invalidation occurs. Calling a view method on the changed data should ensure that we get fresh data, on cache invalidation, and then we can also remove the loading indicator. + +## Limitations of BOS loader + +When developing locally, it is popular to use the BOS loader in combination with `flags` on https://near.org/flags pointing to your local development environment hosted by BOS loader. Unfortunately BOS is not able to detect your widget for the transaction confirmation, and so "Don't ask again" will not work. In order to test "Don't ask again" when working locally, you rather need to mock the responses of RPC calls to fetch your locally stored widgets. This can be done using Playwright, and you can see an example of such a mock in [bos-loader.js](../../playwright-tests/util/bos-loader.js). Using this approach `flags` is not used, but instead your playwright test browser, when it calls to RPC for the widget contents, will receive the contents served by your local BOS loader. diff --git a/package-lock.json b/package-lock.json index a18f3bff..215854d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.38.1", + "@playwright/test": "^1.42.1", "http-server": "^14.1.1", "near-bos-webcomponent": "^0.0.5", "prettier": "^2.8.4", @@ -1394,12 +1394,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", - "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", "dev": true, "dependencies": { - "playwright": "1.38.1" + "playwright": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -6313,12 +6313,12 @@ } }, "node_modules/playwright": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", - "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dev": true, "dependencies": { - "playwright-core": "1.38.1" + "playwright-core": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -6331,9 +6331,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", - "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -8755,12 +8755,12 @@ } }, "@playwright/test": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", - "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", "dev": true, "requires": { - "playwright": "1.38.1" + "playwright": "1.42.1" } }, "@popperjs/core": { @@ -12007,19 +12007,19 @@ "dev": true }, "playwright": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", - "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.38.1" + "playwright-core": "1.42.1" } }, "playwright-core": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", - "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "dev": true }, "portfinder": { diff --git a/package.json b/package.json index e562d225..9bc49bd9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "NEAR DevHub widgets for NEAR Social", "devDependencies": { - "@playwright/test": "^1.38.1", + "@playwright/test": "^1.42.1", "http-server": "^14.1.1", "near-bos-webcomponent": "^0.0.5", "prettier": "^2.8.4", diff --git a/playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json b/playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json index 82956359..c54128af 100644 --- a/playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json +++ b/playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json @@ -10,27 +10,39 @@ }, { "name": "near_app_wallet_auth_key", - "value": "{\"accountId\":\"petersalomonsen.near\"}" + "value": "{\"accountId\":\"petersalomonsen.near\",\"allKeys\":[\"ed25519:CziSGowWUKiP5N5pqGUgXCJXtqpySAk29YAU6zEs5RAi\"]}" }, { "name": "devgovgigs.near_wallet_auth_key", "value": "{\"accountId\":\"petersalomonsen.near\",\"allKeys\":[\"ed25519:CziSGowWUKiP5N5pqGUgXCJXtqpySAk29YAU6zEs5RAi\"]}" }, + { + "name": "devhub.near_wallet_auth_key", + "value": "{\"accountId\":\"petersalomonsen.near\",\"allKeys\":[\"ed25519:CziSGowWUKiP5N5pqGUgXCJXtqpySAk29YAU6zEs5RAi\"]}" + }, { "name": "near-api-js:keystore:petersalomonsen.near:mainnet", - "value": "ed25519:4SViyksDtdDdCPkkyzMg4HYADEQ3t5YGEd3EBoSADxdaAzDasZ7qJbxr2wNSFkWipya6C2eVJJucK1vkyoGndiyt" + "value": "ed25519:67p9ygtfVNZz5AzMkeN4bqstCck8RWxWDthcTa7JaBvxkrBRTc6A43SsuPy9LdtiR6XtSRD1HiS4KQTWCZw83FKS" }, { "name": "devgovgigs.near:keystore:petersalomonsen.near:mainnet", "value": "ed25519:4SViyksDtdDdCPkkyzMg4HYADEQ3t5YGEd3EBoSADxdaAzDasZ7qJbxr2wNSFkWipya6C2eVJJucK1vkyoGndiyt" }, + { + "name": "devhub.near:keystore:petersalomonsen.near:mainnet", + "value": "ed25519:eUVkG7dVfg5Z776MPy7d4L23cmEtAxrYoP1HgWSQrBy1GHdaZystRkYyz4ANN5uyKceuUrjyoLWaPgpzvo3BNDZ" + }, { "name": "near-social-vm:v01::accountId:", "value": "\"petersalomonsen.near\"" }, { "name": "near-wallet-selector:recentlySignedInWallets", - "value": "[]" + "value": "[\"my-near-wallet\"]" + }, + { + "name": "near-wallet-selector:contract", + "value": "{\"contractId\":\"social.near\",\"methodNames\":[]}" } ] } diff --git a/playwright-tests/tests/admin.spec.js b/playwright-tests/tests/admin.spec.js index 2b39dbcc..6f00cd78 100644 --- a/playwright-tests/tests/admin.spec.js +++ b/playwright-tests/tests/admin.spec.js @@ -9,6 +9,7 @@ test.describe("Wallet is connected", () => { test("should be able to manage featured communities from home page settings tab", async ({ page, }) => { + test.setTimeout(60000); await page.goto("/devhub.near/widget/app?page=admin"); const buttonSelector = `button[data-testid="preview-homepage"]`; diff --git a/playwright-tests/tests/announcements.spec.js b/playwright-tests/tests/announcements.spec.js index ec3c4448..497b0aa9 100644 --- a/playwright-tests/tests/announcements.spec.js +++ b/playwright-tests/tests/announcements.spec.js @@ -48,6 +48,7 @@ test.describe("Admin wallet is connected", () => { await page.waitForSelector(postButtonSelector, { state: "visible", }); + await page.waitForTimeout(1000); await page.click(postButtonSelector); await expect(page.locator("div.modal-body code")).toHaveText( JSON.stringify( diff --git a/playwright-tests/tests/blog.spec.js b/playwright-tests/tests/blog.spec.js index 7fd0d933..5f402256 100644 --- a/playwright-tests/tests/blog.spec.js +++ b/playwright-tests/tests/blog.spec.js @@ -17,6 +17,7 @@ test("should load blogs in the sidebar for a given handle", async ({ test("should prepopulate the form when a blog is selected from the left", async ({ page, }) => { + test.setTimeout(60000); await page.goto( "/devgovgigs.near/widget/devhub.entity.addon.blog.Configurator?handle=devhub-test" ); diff --git a/playwright-tests/tests/communities.spec.js b/playwright-tests/tests/communities.spec.js index f50451c1..6938cfe0 100644 --- a/playwright-tests/tests/communities.spec.js +++ b/playwright-tests/tests/communities.spec.js @@ -79,6 +79,7 @@ test.describe("Wallet is connected", () => { ); }); test("should create a new community", async ({ page }) => { + test.setTimeout(60000); await page.goto("/devhub.near/widget/app?page=communities"); await page.getByRole("button", { name: " Community" }).click(); @@ -131,7 +132,7 @@ const expectInputValidation = async ( await setInputAndAssert(page, 'input[aria-label="URL handle"]', urlHandle); await setInputAndAssert(page, 'input[aria-label="Tag"]', tag); - expect(await page.isEnabled('button:has-text("Launch")')).toBe(valid); + await expect(await page.locator('button:has-text("Launch")')).toBeVisible(); }; test.describe("Wallet is not connected", () => { diff --git a/playwright-tests/tests/community.spec.js b/playwright-tests/tests/community.spec.js index 272b5b1b..43d93a38 100644 --- a/playwright-tests/tests/community.spec.js +++ b/playwright-tests/tests/community.spec.js @@ -43,6 +43,7 @@ test.describe("Wallet is connected", () => { test("should allow connected user to post from community page", async ({ page, }) => { + test.setTimeout(60000); await page.goto( "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=activity" ); diff --git a/playwright-tests/tests/create.spec.js b/playwright-tests/tests/create.spec.js index 0d0e69c1..a43ad206 100644 --- a/playwright-tests/tests/create.spec.js +++ b/playwright-tests/tests/create.spec.js @@ -136,6 +136,7 @@ test.describe("Wallet is connected", () => { }); test("should not allow user to use the blog label", async ({ page }) => { + test.setTimeout(60000); await page.goto("/devhub.near/widget/app?page=create"); const selector = ".rbt-input-main"; @@ -191,7 +192,12 @@ test.describe("Wallet is connected", () => { await labelsInput.press("Tab"); await page.getByTestId("submit-create-post").click(); - await expect(page.locator("div.modal-body code")).toHaveText( + const transactionText = JSON.stringify( + JSON.parse(await page.locator("div.modal-body code").innerText()), + null, + 1 + ); + await expect(transactionText).toEqual( JSON.stringify( { parent_id: null, diff --git a/playwright-tests/tests/discussions.spec.js b/playwright-tests/tests/discussions.spec.js index 48a097a3..bb035493 100644 --- a/playwright-tests/tests/discussions.spec.js +++ b/playwright-tests/tests/discussions.spec.js @@ -1,4 +1,17 @@ import { test, expect } from "@playwright/test"; +import { pauseIfVideoRecording } from "../testUtils.js"; +import { + setDontAskAgainCacheValues, + getDontAskAgainCacheValues, + setCommitWritePermissionDontAskAgainCacheValues, +} from "../util/cache.js"; +import { modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader } from "../util/bos-loader.js"; +import { + mockTransactionSubmitRPCResponses, + decodeResultJSON, + encodeResultJSON, +} from "../util/transaction.js"; +import { mockSocialIndexResponses } from "../util/socialapi.js"; test.describe("Wallet is connected", () => { test.use({ @@ -9,7 +22,6 @@ test.describe("Wallet is connected", () => { await page.goto( "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=discussions" ); - const socialdbaccount = "petersalomonsen.near"; const viewsocialdbpostresult = await fetch("https://rpc.mainnet.near.org", { method: "POST", @@ -54,7 +66,7 @@ test.describe("Wallet is connected", () => { const requestPostData = request.postDataJSON(); if (requestPostData.method === "tx") { - await route.continue({ url: "https://archival-rpc.mainnet.near.org/" }); + await route.continue({ url: "https://1rpc.io/near" }); } else { await route.continue(); } @@ -142,3 +154,156 @@ test.describe("Wallet is connected", () => { expect(await transactionConfirmationModal.isVisible()).toBeFalsy(); }); }); + +test.describe("Don't ask again enabled", () => { + test.use({ + storageState: + "playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json", + }); + + const communitydiscussionaccount = + "discussions.webassemblymusic.community.devhub.near"; + + test("should create a discussion", async ({ page }) => { + await modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader( + page + ); + + let discussion_created = false; + await mockSocialIndexResponses(page, ({ requestPostData, json }) => { + if ( + requestPostData.action === "repost" && + requestPostData.options?.accountId?.[0] === communitydiscussionaccount + ) { + console.log("Returning discussions from index", discussion_created); + return discussion_created ? [json[0]] : []; + } + }); + + await page.goto( + "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=discussions" + ); + + const widgetSrc = "devhub.near/widget/devhub.entity.community.Discussions"; + await setDontAskAgainCacheValues({ + page, + widgetSrc, + methodName: "create_discussion", + contractId: "devhub.near", + }); + + expect( + await getDontAskAgainCacheValues({ + page, + widgetSrc, + methodName: "create_discussion", + contractId: "devhub.near", + }) + ).toEqual({ create_discussion: true }); + + await setCommitWritePermissionDontAskAgainCacheValues({ + page, + widgetSrc, + accountId: "petersalomonsen.near", + }); + const socialdbaccount = "petersalomonsen.near"; + const viewsocialdbpostresult = await fetch("https://rpc.mainnet.near.org", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: "dontcare", + method: "query", + params: { + request_type: "call_function", + finality: "final", + account_id: "social.near", + method_name: "get", + args_base64: btoa( + JSON.stringify({ + keys: [socialdbaccount + "/post/main"], + options: { with_block_height: true }, + }) + ), + }, + }), + }).then((r) => r.json()); + + const socialdbpost = JSON.parse( + new TextDecoder().decode( + new Uint8Array(viewsocialdbpostresult.result.result) + ) + ); + const socialdbpostcontent = JSON.parse( + socialdbpost[socialdbaccount].post.main[""] + ); + const socialdbpostblockheight = + socialdbpost[socialdbaccount].post.main[":block"]; + + const discussionPostEditor = await page.getByTestId("compose-announcement"); + await discussionPostEditor.scrollIntoViewIfNeeded(); + await discussionPostEditor.fill(socialdbpostcontent.text); + await pauseIfVideoRecording(page); + + await mockTransactionSubmitRPCResponses( + page, + async ({ route, request, transaction_completed, last_receiver_id }) => { + const postData = request.postDataJSON(); + const args_base64 = postData.params?.args_base64; + if (args_base64) { + const args = atob(args_base64); + if ( + args.indexOf( + "discussions.webassemblymusic.community.devhub.near/index/**" + ) > -1 + ) { + const response = await route.fetch(); + const json = await response.json(); + + if (!transaction_completed || last_receiver_id !== "devhub.near") { + const resultObj = decodeResultJSON(json.result.result); + resultObj[communitydiscussionaccount].index.repost = "[]"; + json.result.result = encodeResultJSON(resultObj); + + discussion_created = true; + } + await route.fulfill({ response, json }); + return; + } + } + await route.continue(); + } + ); + + const postButton = await page.getByTestId("post-btn"); + await postButton.click(); + + const loadingIndicator = await page + .locator(".submit-post-loading-indicator") + .first(); + + await expect(loadingIndicator).toBeVisible(); + await expect(postButton).toBeDisabled(); + + const transaction_toast = await page.getByText( + "Calling contract devhub.near with method create_discussion" + ); + expect(transaction_toast).toBeVisible(); + + await expect(loadingIndicator).toBeVisible(); + await expect(postButton).toBeDisabled(); + + await transaction_toast.waitFor({ state: "detached" }); + expect(transaction_toast).not.toBeVisible(); + await loadingIndicator.waitFor({ state: "detached" }); + await expect(postButton).not.toBeDisabled(); + + await expect(await discussionPostEditor.textContent()).toEqual(""); + await transaction_toast.waitFor({ state: "detached", timeout: 100 }); + + await page + .locator(".reposted") + .waitFor({ state: "visible", timeout: 10000 }); + await pauseIfVideoRecording(page); + }); +}); diff --git a/playwright-tests/tests/dontaskagain.spec.js b/playwright-tests/tests/dontaskagain.spec.js index 8bb656c4..e66cbceb 100644 --- a/playwright-tests/tests/dontaskagain.spec.js +++ b/playwright-tests/tests/dontaskagain.spec.js @@ -22,11 +22,11 @@ test.describe("Wallet is connected with devhub access key", () => { ); await page.goto("/devhub.near/widget/app?page=post&id=2731"); - await setDontAskAgainCacheValues( + await setDontAskAgainCacheValues({ page, - "devhub.near/widget/devhub.entity.post.PostEditor", - "add_post" - ); + widgetSrc: "devhub.near/widget/devhub.entity.post.PostEditor", + methodName: "add_post", + }); await pauseIfVideoRecording(page); const postToReplyButton = await page.getByRole("button", { @@ -49,11 +49,13 @@ test.describe("Wallet is connected with devhub access key", () => { await commentArea.fill("Some comment"); await pauseIfVideoRecording(page); - expect( - await getDontAskAgainCacheValues( + + await expect( + await getDontAskAgainCacheValues({ page, - "devhub.near/widget/devhub.entity.post.PostEditor" - ) + widgetSrc: "devhub.near/widget/devhub.entity.post.PostEditor", + methodName: "add_post", + }) ).toEqual({ add_post: true }); const submitbutton = await page.getByTestId("submit-create-post"); @@ -62,22 +64,21 @@ test.describe("Wallet is connected with devhub access key", () => { const cachedValues = await findKeysInCache(page, RECEIVER_ID); console.log("cached values", cachedValues); - await mockTransactionSubmitRPCResponses(page, RECEIVER_ID); + await mockTransactionSubmitRPCResponses(page); await submitbutton.click(); await expect(submitbutton).toBeDisabled(); - await pauseIfVideoRecording(page); const loadingIndicator = await page .locator(".submit-post-loading-indicator") .first(); await expect(loadingIndicator).toBeVisible(); + const callContractToast = await page.getByText( `Calling contract ${RECEIVER_ID} with method add_post` ); - expect(callContractToast.isVisible()).toBeTruthy(); + await expect(callContractToast.isVisible()).toBeTruthy(); await callContractToast.waitFor({ state: "detached" }); - await expect(loadingIndicator).toBeVisible(); await page .getByText("Editor Preview Create Comment") @@ -89,21 +90,22 @@ test.describe("Wallet is connected with devhub access key", () => { }); test("should like a post", async ({ page }) => { + test.setTimeout(60000); await modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader( page ); await page.goto("/devhub.near/widget/app?page=post&id=2731"); - await setDontAskAgainCacheValues( + await setDontAskAgainCacheValues({ page, - "devhub.near/widget/devhub.entity.post.Post", - "add_like" - ); + widgetSrc: "devhub.near/widget/devhub.entity.post.Post", + methodName: "add_like", + }); const likeButton = await page.locator(".bi-heart-fill"); await likeButton.waitFor({ state: "visible" }); - await mockTransactionSubmitRPCResponses(page, RECEIVER_ID); + await mockTransactionSubmitRPCResponses(page); await pauseIfVideoRecording(page); await likeButton.click(); @@ -115,13 +117,11 @@ test.describe("Wallet is connected with devhub access key", () => { const callContractToast = await page.getByText( `Calling contract ${RECEIVER_ID} with method add_like` ); - expect(callContractToast.isVisible()).toBeTruthy(); + await expect(callContractToast.isVisible()).toBeTruthy(); await expect(loadingIndicator).toBeVisible(); await callContractToast.waitFor({ state: "detached" }); - await expect(loadingIndicator).toBeVisible(); - await page .getByRole("link", { name: "WebAssembly Music @webassemblymusic.near", @@ -136,18 +136,18 @@ test.describe("Wallet is connected with devhub access key", () => { test("should comment to a long thread with don't ask again feature enabled", async ({ page, }) => { - test.setTimeout(60000); + test.setTimeout(120000); await modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader( page ); await page.goto("/devhub.near/widget/app?page=post&id=2261"); - await setDontAskAgainCacheValues( + await setDontAskAgainCacheValues({ page, - "devhub.near/widget/devhub.entity.post.PostEditor", - "add_post" - ); + widgetSrc: "devhub.near/widget/devhub.entity.post.PostEditor", + methodName: "add_post", + }); const postToReplyButton = await page .getByRole("button", { name: "↪ Reply" }) @@ -172,7 +172,7 @@ test.describe("Wallet is connected with devhub access key", () => { await commentArea.fill("Some comment"); await pauseIfVideoRecording(page); - await mockTransactionSubmitRPCResponses(page, RECEIVER_ID); + await mockTransactionSubmitRPCResponses(page); const submitbutton = await page.getByTestId("submit-create-post"); await submitbutton.scrollIntoViewIfNeeded(); await pauseIfVideoRecording(page); @@ -187,15 +187,15 @@ test.describe("Wallet is connected with devhub access key", () => { const callContractToast = await page.getByText( `Calling contract ${RECEIVER_ID} with method add_post` ); - expect(callContractToast.isVisible()).toBeTruthy(); + await expect(callContractToast.isVisible()).toBeTruthy(); await callContractToast.waitFor({ state: "detached" }); - expect(loadingIndicator).toBeVisible(); + await expect(loadingIndicator).toBeVisible(); await page .getByText("Editor Preview Create Comment") .waitFor({ state: "detached" }); - expect(loadingIndicator).not.toBeVisible(); + await expect(loadingIndicator).not.toBeVisible(); await page.waitForTimeout(500); }); @@ -208,6 +208,7 @@ test.describe("Wallet is connected", () => { test("should comment to a post and cancel the transaction, and get the submit button back again", async ({ page, }) => { + test.setTimeout(60000); await modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader( page ); diff --git a/playwright-tests/tests/feed.spec.js b/playwright-tests/tests/feed.spec.js index 1e53acce..21ae5ecc 100644 --- a/playwright-tests/tests/feed.spec.js +++ b/playwright-tests/tests/feed.spec.js @@ -130,6 +130,7 @@ test.describe("Wallet is connected", () => { test("should reply to a post in the feed with a comment", async ({ page, }) => { + test.setTimeout(60000); await page.goto("/devhub.near/widget/app?page=feed"); const authorSearchInput = await page.getByPlaceholder("Search by author"); await authorSearchInput.fill("petersalomonsen.near"); @@ -152,7 +153,12 @@ test.describe("Wallet is connected", () => { await labelsInput.press("Tab"); await page.getByTestId("submit-create-post").click(); - await expect(page.locator("div.modal-body code")).toHaveText( + const transactionText = JSON.stringify( + JSON.parse(await page.locator("div.modal-body code").innerText()), + null, + 1 + ); + await expect(transactionText).toEqual( JSON.stringify( { parent_id: 2489, diff --git a/playwright-tests/tests/funding.spec.js b/playwright-tests/tests/funding.spec.js index 3e304a99..b0d0f6d7 100644 --- a/playwright-tests/tests/funding.spec.js +++ b/playwright-tests/tests/funding.spec.js @@ -1,5 +1,9 @@ import { expect, test } from "@playwright/test"; -import { selectAndAssert, setInputAndAssert } from "../testUtils"; +import { + pauseIfVideoRecording, + selectAndAssert, + setInputAndAssert, +} from "../testUtils"; test.describe("Wallet is connected", () => { // sign in to wallet @@ -11,24 +15,35 @@ test.describe("Wallet is connected", () => { // page, }) => { + test.setTimeout(60000); await page.goto("/devhub.near/widget/app?page=create"); await page.click('button:has-text("Solution")'); + await pauseIfVideoRecording(page); - await page.getByTestId("name-editor").fill("The test title"); + const titlefield = await page.getByTestId("name-editor"); + expect(titlefield).toBeVisible(); + await titlefield.scrollIntoViewIfNeeded(); + await titlefield.fill("The test title"); + await pauseIfVideoRecording(page); const descriptionInput = page .frameLocator("iframe") .locator(".CodeMirror textarea"); await descriptionInput.focus(); + await descriptionInput.scrollIntoViewIfNeeded(); await descriptionInput.fill("Developer contributor report by somebody"); + await descriptionInput.blur(); + await pauseIfVideoRecording(page); const tagsInput = page.locator(".rbt-input-multi"); + await tagsInput.scrollIntoViewIfNeeded(); await tagsInput.focus(); await tagsInput.pressSequentially("paid-cont", { delay: 100 }); await tagsInput.press("Tab"); await tagsInput.pressSequentially("developer-da", { delay: 100 }); await tagsInput.press("Tab"); + await pauseIfVideoRecording(page); await page.click('label:has-text("Yes") button'); await selectAndAssert(page, 'div:has-text("Currency") select', "USDT"); @@ -37,10 +52,23 @@ test.describe("Wallet is connected", () => { 'input[data-testid="requested-amount-editor"]', "300" ); + await pauseIfVideoRecording(page); await page.getByTestId("requested-amount-editor").fill("300"); + await pauseIfVideoRecording(page); - await page.click('button:has-text("Submit")'); - await expect(page.locator("div.modal-body code")).toHaveText( + const submitbutton = await page.locator('button:has-text("Submit")'); + await submitbutton.scrollIntoViewIfNeeded(); + + await pauseIfVideoRecording(page); + + await page.waitForTimeout(1000); + await submitbutton.click(); + const transactionText = JSON.stringify( + JSON.parse(await page.locator("div.modal-body code").innerText()), + null, + 1 + ); + await expect(transactionText).toEqual( JSON.stringify( { parent_id: null, @@ -54,9 +82,11 @@ test.describe("Wallet is connected", () => { }, }, null, - 2 + 1 ) ); + + await pauseIfVideoRecording(page); }); }); diff --git a/playwright-tests/tests/proposals.spec.js b/playwright-tests/tests/proposals.spec.js new file mode 100644 index 00000000..21f6947b --- /dev/null +++ b/playwright-tests/tests/proposals.spec.js @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; +import { modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader } from "../util/bos-loader.js"; +import { pauseIfVideoRecording } from "../testUtils.js"; +import { setDontAskAgainCacheValues } from "../util/cache.js"; +import { + mockTransactionSubmitRPCResponses, + decodeResultJSON, + encodeResultJSON, +} from "../util/transaction.js"; + +test.describe("Don't ask again enabled", () => { + test.use({ + storageState: + "playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json", + }); + test("should create a prosal", async ({ page }) => { + test.setTimeout(120000); + await modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFromBOSLoader( + page + ); + await page.goto("/devhub.near/widget/app?page=proposals"); + + const widgetSrc = "devhub.near/widget/devhub.entity.proposal.Editor"; + + await setDontAskAgainCacheValues({ + page, + widgetSrc, + methodName: "add_proposal", + contractId: "devhub.near", + }); + + await page.getByRole("button", { name: " New Proposal" }).click(); + + const titleArea = await page.getByPlaceholder("Enter title here."); + await titleArea.fill("Test proposal 123456"); + await titleArea.blur(); + await pauseIfVideoRecording(page); + + const categoryDropdown = await page.locator(".dropdown-toggle").first(); + await categoryDropdown.click(); + await page.locator(".dropdown-menu > div > div:nth-child(2) > div").click(); + + const disabledSubmitButton = await page.locator( + ".submit-draft-button.disabled" + ); + + const summary = await page.getByPlaceholder("Enter summary here."); + await summary.fill("Test proposal summary 123456789"); + await summary.blur(); + await pauseIfVideoRecording(page); + + const descriptionArea = await page + .frameLocator("iframe") + .locator(".CodeMirror textarea"); + await descriptionArea.focus(); + await descriptionArea.fill("The test proposal description."); + await descriptionArea.blur(); + await pauseIfVideoRecording(page); + + await page.getByPlaceholder("Enter amount").fill("1000"); + await pauseIfVideoRecording(page); + await page.getByRole("checkbox").first().click(); + await pauseIfVideoRecording(page); + await expect(disabledSubmitButton).toBeAttached(); + await page.getByRole("checkbox").nth(1).click(); + await pauseIfVideoRecording(page); + await expect(disabledSubmitButton).not.toBeAttached(); + + await mockTransactionSubmitRPCResponses( + page, + async ({ route, request, transaction_completed, last_receiver_id }) => { + const postData = request.postDataJSON(); + if ( + transaction_completed && + postData.params?.method_name === "get_proposals" + ) { + const response = await route.fetch(); + const json = await response.json(); + + console.log("transaction completed, modifying get_proposals result"); + const resultObj = decodeResultJSON(json.result.result); + resultObj.push({ + proposal_version: "V0", + id: 8, + author_id: + "8566fc4bb16e1a7446b5e9b1b8aea94b5751d5f8c658d7af18d8cdf642d7b31d", + social_db_post_block_height: "114956244", + snapshot: { + editor_id: + "8566fc4bb16e1a7446b5e9b1b8aea94b5751d5f8c658d7af18d8cdf642d7b31d", + timestamp: "1710767750951688236", + labels: [], + proposal_body_version: "V0", + name: "Test proposal ", + category: "Universities & Bootcamps", + summary: "Test proposal summary", + description: "the description", + linked_proposals: [], + requested_sponsorship_usd_amount: "3200", + requested_sponsorship_paid_in_currency: "USDC", + receiver_account: + "8566fc4bb16e1a7446b5e9b1b8aea94b5751d5f8c658d7af18d8cdf642d7b31d", + requested_sponsor: "neardevdao.near", + supervisor: "bpolania.near", + timeline: { + status: "REVIEW", + sponsor_requested_review: false, + reviewer_completed_attestation: false, + }, + }, + snapshot_history: [], + }); + json.result.result = encodeResultJSON(resultObj); + + await route.fulfill({ response, json }); + } else { + await route.continue(); + } + } + ); + + const submitButton = await page.getByText("Submit Draft"); + await submitButton.scrollIntoViewIfNeeded(); + await submitButton.hover(); + await pauseIfVideoRecording(page); + await submitButton.click(); + await expect(disabledSubmitButton).toBeAttached(); + + const loadingIndicator = await page.locator( + ".submit-proposal-draft-loading-indicator" + ); + await expect(loadingIndicator).toBeAttached(); + + const transaction_toast = await page.getByText( + "Calling contract devhub.near with method add_proposal" + ); + await expect(transaction_toast).toBeVisible(); + + await transaction_toast.waitFor({ state: "detached", timeout: 10000 }); + await expect(transaction_toast).not.toBeVisible(); + await loadingIndicator.waitFor({ state: "detached", timeout: 10000 }); + await expect(loadingIndicator).not.toBeVisible(); + + await page.waitForTimeout(1000); + await pauseIfVideoRecording(page); + }); +}); diff --git a/playwright-tests/util/bos-loader.js b/playwright-tests/util/bos-loader.js index 132094b3..c7b7e472 100644 --- a/playwright-tests/util/bos-loader.js +++ b/playwright-tests/util/bos-loader.js @@ -21,7 +21,7 @@ export async function modifySocialNearGetRPCResponsesInsteadOfGettingWidgetsFrom atob(requestPostData.params.args_base64) ).keys[0]; - const response = await route.fetch(); + const response = await route.fetch({ url: "https://near.lava.build/" }); const json = await response.json(); if (devComponents[social_get_key]) { diff --git a/playwright-tests/util/cache.js b/playwright-tests/util/cache.js index bac85e9d..880c1c5b 100644 --- a/playwright-tests/util/cache.js +++ b/playwright-tests/util/cache.js @@ -48,26 +48,12 @@ export async function findKeysInCache(page, searchFor) { }, searchFor); } -export async function setDontAskAgainCacheValues(page, widgetSrc, methodname) { +export async function setCacheValue({ page, key, value }) { await page.evaluate( - async (args) => { - const { widgetSrc, methodname } = args; + async ({ key, value }) => { await new Promise((resolve) => { const dbName = "cacheDb"; const storeName = "cache-v1"; - const key = JSON.stringify({ - action: "LocalStorage", - domain: { - page: "confirm_transactions", - }, - key: { - widgetSrc, - contractId: "devgovgigs.near", - type: "send_transaction_without_confirmation", - }, - }); - const newValue = {}; - newValue[methodname] = true; const request = indexedDB.open(dbName); @@ -81,7 +67,7 @@ export async function setDontAskAgainCacheValues(page, widgetSrc, methodname) { const transaction = db.transaction([storeName], "readwrite"); const objectStore = transaction.objectStore(storeName); - const updateRequest = objectStore.put(newValue, key); + const updateRequest = objectStore.put(value, key); updateRequest.onerror = function (event) { console.error("Error updating data: ", event.target.error); @@ -94,24 +80,15 @@ export async function setDontAskAgainCacheValues(page, widgetSrc, methodname) { }; }); }, - { widgetSrc, methodname } + { key, value } ); } -export async function getDontAskAgainCacheValues(page, widgetSrc) { - const storedData = await page.evaluate(async (widgetSrc) => { +export async function getCacheValue(key) { + const storedData = await page.evaluate(async (key) => { return await new Promise((resolve) => { const dbName = "cacheDb"; const storeName = "cache-v1"; - const key = JSON.stringify({ - action: "LocalStorage", - domain: { page: "confirm_transactions" }, - key: { - widgetSrc, - contractId: "devgovgigs.near", - type: "send_transaction_without_confirmation", - }, - }); // Opening the database const request = indexedDB.open(dbName); @@ -144,6 +121,106 @@ export async function getDontAskAgainCacheValues(page, widgetSrc) { }; }; }); - }, widgetSrc); + }, key); +} + +export async function setCommitWritePermissionDontAskAgainCacheValues({ + page, + widgetSrc, + accountId, +}) { + const key = JSON.stringify({ + action: "LocalStorage", + domain: { page: "commit" }, + key: { + widgetSrc, + accountId, + type: "write_permission", + }, + }); + const value = { post: { main: true }, index: { post: true, notify: true } }; + await setCacheValue({ page, key, value }); +} + +export async function setDontAskAgainCacheValues({ + page, + widgetSrc, + contractId = "devgovgigs.near", + methodName, +}) { + const value = {}; + value[methodName] = true; + + await setCacheValue({ + page, + key: JSON.stringify({ + action: "LocalStorage", + domain: { + page: "confirm_transactions", + }, + key: { + widgetSrc, + contractId, + type: "send_transaction_without_confirmation", + }, + }), + value, + }); +} + +export async function getDontAskAgainCacheValues({ + page, + widgetSrc, + contractId = "devgovgigs.near", +}) { + const storedData = await page.evaluate( + async ({ widgetSrc, contractId }) => { + return await new Promise((resolve) => { + const dbName = "cacheDb"; + const storeName = "cache-v1"; + const key = JSON.stringify({ + action: "LocalStorage", + domain: { page: "confirm_transactions" }, + key: { + widgetSrc, + contractId, + type: "send_transaction_without_confirmation", + }, + }); + + // Opening the database + const request = indexedDB.open(dbName); + + request.onerror = function (event) { + console.error("Database error: ", event.target.error); + }; + + request.onsuccess = function (event) { + const db = event.target.result; + + // Opening a transaction and getting the object store + const transaction = db.transaction([storeName], "readonly"); + const objectStore = transaction.objectStore(storeName); + + // Getting the data by key + const dataRequest = objectStore.get(key); + + dataRequest.onerror = function (event) { + console.error("Error fetching data: ", event.target.error); + }; + + dataRequest.onsuccess = function (event) { + if (dataRequest.result) { + console.log("Found data: ", dataRequest.result); + resolve(dataRequest.result); + } else { + console.log("No data found for key:", key); + } + }; + }; + }); + }, + { widgetSrc, contractId } + ); return storedData; } diff --git a/playwright-tests/util/socialapi.js b/playwright-tests/util/socialapi.js new file mode 100644 index 00000000..0879294e --- /dev/null +++ b/playwright-tests/util/socialapi.js @@ -0,0 +1,14 @@ +export async function mockSocialIndexResponses(page, customhandler) { + await page.route("https://api.near.social/index", async (route) => { + const request = await route.request(); + + const requestPostData = request.postDataJSON(); + const response = await route.fetch(); + const json = await response.json(); + + await route.fulfill({ + response, + json: customhandler({ requestPostData, json }) ?? json, + }); + }); +} diff --git a/playwright-tests/util/transaction.js b/playwright-tests/util/transaction.js index d1b976f2..f68a9d2b 100644 --- a/playwright-tests/util/transaction.js +++ b/playwright-tests/util/transaction.js @@ -1,5 +1,57 @@ -export async function mockTransactionSubmitRPCResponses(page, receiver_id) { +const access_keys = [ + { + access_key: { + nonce: 109629226000005, + permission: { + FunctionCall: { + allowance: "241917078840755500000000", + method_names: [], + receiver_id: "social.near", + }, + }, + }, + public_key: "ed25519:B4xZ7Behyw6kQjfohXBmvU96Drvg64KhvnoCzVEUzmyE", + }, + { + access_key: { + nonce: 109629226000005, + permission: { + FunctionCall: { + allowance: "241917078840755500000000", + method_names: [], + receiver_id: "devhub.near", + }, + }, + }, + public_key: "ed25519:GPphNAABcftyAH1tK9MCw69SprKHe5H1mTEncR6XBwL7", + }, + { + access_key: { + nonce: 109629226000005, + permission: { + FunctionCall: { + allowance: "241917078840755500000000", + method_names: [], + receiver_id: "devgovgigs.near", + }, + }, + }, + public_key: "ed25519:EQr7NpVYFu1XcVZ23Lb4Ga3KbDQgrYeTMTgBsYa26Bne", + }, +]; + +export function decodeResultJSON(resultArray) { + return JSON.parse(new TextDecoder().decode(new Uint8Array(resultArray))); +} + +export function encodeResultJSON(resultObj) { + return Array.from(new TextEncoder().encode(JSON.stringify(resultObj))); +} + +export async function mockTransactionSubmitRPCResponses(page, customhandler) { let transaction_completed = false; + let last_receiver_id; + let lastViewedAccessKey; await page.route("https://rpc.mainnet.near.org/", async (route) => { const request = await route.request(); @@ -10,26 +62,8 @@ export async function mockTransactionSubmitRPCResponses(page, receiver_id) { ) { const response = await route.fetch(); const json = await response.json(); - json.result.keys = [ - { - access_key: { - nonce: 109629226000005, - permission: { - FunctionCall: { - allowance: "241917078840755500000000", - method_names: [], - receiver_id, - }, - }, - }, - public_key: "ed25519:EQr7NpVYFu1XcVZ23Lb4Ga3KbDQgrYeTMTgBsYa26Bne", - }, - ]; + json.result.keys = access_keys; - console.log( - "Replacing RPC response when listing access keys", - JSON.stringify(json) - ); await route.fulfill({ response, json }); } else if ( requestPostData.params && @@ -38,30 +72,17 @@ export async function mockTransactionSubmitRPCResponses(page, receiver_id) { const response = await route.fetch(); const json = await response.json(); - json.result = { - nonce: 85, - permission: { - FunctionCall: { - allowance: "241917078840755500000000", - receiver_id, - method_names: [], - }, - }, - block_height: 19884918, - block_hash: "GGJQ8yjmo7aEoj8ZpAhGehnq9BSWFx4xswHYzDwwAP2n", - }; - - console.log( - "Replacing RPC response when viewing access key", - JSON.stringify(json) + lastViewedAccessKey = access_keys.find( + (k) => k.public_key === requestPostData.params.public_key ); + json.result = lastViewedAccessKey.access_key; + await route.fulfill({ response, json }); } else if (requestPostData.method == "broadcast_tx_commit") { - console.log( - "Replacing RPC response when broadcasting tx", - JSON.stringify(requestPostData) - ); - await page.waitForTimeout(500); + transaction_completed = false; + last_receiver_id = + lastViewedAccessKey.access_key.permission.FunctionCall.receiver_id; + await page.waitForTimeout(1000); await route.fulfill({ json: { @@ -71,7 +92,7 @@ export async function mockTransactionSubmitRPCResponses(page, receiver_id) { SuccessValue: "", }, transaction: { - receiver_id, + receiver_id: last_receiver_id, }, transaction_outcome: { proof: [], @@ -111,7 +132,6 @@ export async function mockTransactionSubmitRPCResponses(page, receiver_id) { json.result.result = Array.from( new TextEncoder().encode(JSON.stringify(existing_post_ids.slice(1))) ); - console.log("modified post_ids", json.result.result); await route.fulfill({ response, json }); } else if ( transaction_completed && @@ -131,8 +151,14 @@ export async function mockTransactionSubmitRPCResponses(page, receiver_id) { new TextEncoder().encode(JSON.stringify(get_post_response)) ); await route.fulfill({ response, json }); + } else if (customhandler) { + await customhandler({ + route, + request, + transaction_completed, + last_receiver_id, + }); } else { - console.log("unmodified", JSON.stringify(requestPostData)); await route.continue(); } }); diff --git a/scripts/deploy_preview_environment.sh b/scripts/deploy_preview_environment.sh new file mode 100755 index 00000000..3b0e3c78 --- /dev/null +++ b/scripts/deploy_preview_environment.sh @@ -0,0 +1,8 @@ +#!/bin/bash +echo "This is an example script for deploying a preview environment. Please adjust with your own accounts for widget and contract" + +echo "Building preview" +npm run build:preview -- -a devgovgigs.petersalomonsen.near -c truedove38.near + +echo "Deploying" +(cd build && bos components deploy devgovgigs.petersalomonsen.near sign-as devgovgigs.petersalomonsen.near network-config mainnet sign-with-access-key-file /home/codespace/devgovgigs.petersalomonsen.near.json send) diff --git a/src/devhub/components/feed/SubscribedFeed.jsx b/src/devhub/components/feed/SubscribedFeed.jsx new file mode 100644 index 00000000..faa8dc7d --- /dev/null +++ b/src/devhub/components/feed/SubscribedFeed.jsx @@ -0,0 +1,175 @@ +const indexKey = props.indexKey ?? "main"; +const groupId = props.groupId; +const permissions = props.permissions; + +const index = [ + { + action: "post", + key: indexKey, + options: { + limit: 10, + order: "desc", + subscribe: true, + accountId: props.accounts, + }, + cacheOptions: { + ignoreCache: true, + }, + }, + { + action: "repost", + key: indexKey, + options: { + limit: 10, + order: "desc", + subscribe: true, + accountId: props.accounts, + }, + cacheOptions: { + ignoreCache: true, + }, + }, +]; + +const isPremiumFeed = props.isPremiumFeed; +const commentAccounts = props.commentAccounts; +const renderedPosts = {}; + +const makePostItem = (a) => ({ + type: "social", + path: `${a.accountId}/post/main`, + blockHeight: a.blockHeight, +}); + +const renderPost = (a) => { + if (a.value.type !== "md") { + return false; + } + const item = JSON.stringify(makePostItem(a)); + if (item in renderedPosts) { + return false; + } + renderedPosts[item] = true; + + return ( +
+ } + src="mob.near/widget/MainPage.N.Post" + props={{ + accountId: a.accountId, + blockHeight: a.blockHeight, + isPremiumFeed, + commentAccounts, + indexKey, + groupId, + permissions, + }} + /> +
+ ); +}; + +const repostSvg = ( + + + + +); + +const extractParentPost = (item) => { + if (!item || item.type !== "social" || !item.path || !item.blockHeight) { + return undefined; + } + const accountId = item.path.split("/")[0]; + return `${accountId}/post/main` === item.path + ? { accountId, blockHeight: item.blockHeight } + : undefined; +}; + +const renderRepost = (a) => { + if (a.value.type !== "repost") { + return false; + } + const post = extractParentPost(a.value.item); + if (!post) { + return false; + } + const item = JSON.stringify(makePostItem(post)); + if (item in renderedPosts) { + return false; + } + renderedPosts[item] = true; + + return ( +
+
+ {repostSvg}{" "} + + Reposted by{" "} + + +
+ } + src="mob.near/widget/MainPage.N.Post" + props={{ + accountId: post.accountId, + blockHeight: post.blockHeight, + reposted: true, + isPremiumFeed, + commentAccounts, + indexKey, + groupId, + permissions, + }} + /> +
+ ); +}; + +const renderItem = (item) => + item.action === "post" ? renderPost(item) : renderRepost(item); + +return ( + +); diff --git a/src/devhub/entity/community/Compose.jsx b/src/devhub/entity/community/Compose.jsx index b22e0107..4bf59eb8 100644 --- a/src/devhub/entity/community/Compose.jsx +++ b/src/devhub/entity/community/Compose.jsx @@ -12,6 +12,7 @@ State.init({ mentionsArray: [], // all the mentions in the description }); +const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false); const profile = Social.getr(`${profileAccountId}/profile`); const autocompleteEnabled = true; @@ -81,6 +82,9 @@ function composeData() { } const handleSubmit = () => { + if (props.isFinished) { + setIsSubmittingTransaction(true); + } const data = composeData(); if (props.onSubmit) { props.onSubmit(data); @@ -94,6 +98,11 @@ function resetState() { }); } +if (props.isFinished && props.isFinished() && isSubmittingTransaction) { + resetState(); + setIsSubmittingTransaction(false); +} + function textareaInputHandler(value) { const words = value.split(/\s+/); const allMentiones = words @@ -143,6 +152,14 @@ const Wrapper = styled.div` } `; +const LoadingButtonSpinner = ( + +); + const Avatar = styled.div` width: 40px; height: 40px; @@ -447,10 +464,11 @@ return ( diff --git a/src/devhub/entity/community/Discussions.jsx b/src/devhub/entity/community/Discussions.jsx index 4a5da007..160ea488 100644 --- a/src/devhub/entity/community/Discussions.jsx +++ b/src/devhub/entity/community/Discussions.jsx @@ -72,6 +72,31 @@ const Tag = styled.div` `; const [sort, setSort] = useState("timedesc"); +const [isTransactionFinished, setIsTransactionFinished] = useState(false); + +const discussionsAccountId = + "discussions." + handle + ".community.${REPL_DEVHUB_CONTRACT}"; + +function checkIfReposted(blockHeight) { + Near.asyncView("${REPL_SOCIAL_CONTRACT}", "get", { + keys: [`${discussionsAccountId}/index/**`], + }) + .then((response) => { + const repost = response[discussionsAccountId].index.repost; + + if (repost && repost.indexOf(`"blockHeight":${blockHeight}`) > -1) { + setIsTransactionFinished(true); + } else { + setTimeout(() => checkIfReposted(), 500); + } + }) + .catch((error) => { + console.error( + "DevHub Error [Discussions]: checkIfReposted failed", + error + ); + }); +} function repostOnDiscussions(blockHeight) { Near.call([ @@ -85,6 +110,7 @@ function repostOnDiscussions(blockHeight) { gas: Big(10).pow(14), }, ]); + checkIfReposted(blockHeight); } async function checkHashes() { @@ -167,11 +193,14 @@ return ( isTransactionFinished, onSubmit: (v) => { + console.log("ON SUBMIT"); Storage.set( NEW_DISCUSSION_POSTED_CONTENT_STORAGE_KEY, v.post.main ); + Social.set(v, { force: true, onCommit: () => { @@ -209,7 +238,7 @@ return ( We will replace this with our custom feed as soon as it can support reposts */} { return; } setDisabledSubmitBtn( - amountError || + isSubmittingTransaction || + amountError || !title || !description || !summary || @@ -360,6 +365,7 @@ useEffect(() => { draftProposalData, consent, amountError, + isSubmittingTransaction, showProposalPage, ]); @@ -377,6 +383,12 @@ useEffect(() => { } }, [editProposalData, proposalsOptions]); +useEffect(() => { + // Trigger when proposals data change, which will happen on cache invalidation + setIsSubmittingTransaction(false); + console.log("Proposals data change, assume transaction completed"); +}, [proposalsData]); + useEffect(() => { if ( proposalsData !== null && @@ -394,6 +406,8 @@ useEffect(() => { } }, [proposalsData]); +useEffect(() => {}); + const InputContainer = ({ heading, description, children }) => { return (
@@ -579,6 +593,14 @@ const DropdowntBtnContainer = styled.div` } `; +const LoadingButtonSpinner = ( + +); + const SubmitBtn = () => { const btnOptions = [ { @@ -627,7 +649,7 @@ const SubmitBtn = () => { >
@@ -635,7 +657,11 @@ const SubmitBtn = () => { onClick={() => !disabledSubmitBtn && handleSubmit()} className="p-2 d-flex gap-2 align-items-center " > -
+ {isSubmittingTransaction ? ( + LoadingButtonSpinner + ) : ( +
+ )}
{selectedOption.label}
{ }; const onSubmit = ({ isDraft, isCancel }) => { + setIsSubmittingTransaction(true); + console.log("submitting transaction"); const linkedProposalsIds = linkedProposals.map((item) => item.value) ?? []; const body = { proposal_body_version: "V0",