new home
@@ -12,12 +12,14 @@ dependencies {
|
||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-app-launcher')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-haptics')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capacitor-toast')
|
||||
implementation project(':capacitor-secure-storage-plugin')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito
|
||||
include ':capacitor-app-launcher'
|
||||
project(':capacitor-app-launcher').projectDir = new File('../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher/android')
|
||||
|
||||
include ':capacitor-camera'
|
||||
project(':capacitor-camera').projectDir = new File('../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera/android')
|
||||
|
||||
include ':capacitor-clipboard'
|
||||
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard/android')
|
||||
|
||||
@@ -28,3 +31,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@c
|
||||
|
||||
include ':capacitor-toast'
|
||||
project(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast/android')
|
||||
|
||||
include ':capacitor-secure-storage-plugin'
|
||||
project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin/android')
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { loadHome, visitSettings } from "./utils";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("http://localhost:3420/");
|
||||
});
|
||||
|
||||
test("test local encrypt", async ({ page }) => {
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
|
||||
// Wait for an element matching the selector to appear in DOM.
|
||||
await page.waitForSelector("text=0 SATS");
|
||||
|
||||
console.log("Page loaded.");
|
||||
|
||||
// Wait for a while just to make sure we can load everything
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Navigate to settings
|
||||
const settingsLink = await page.getByRole("link", { name: "Settings" });
|
||||
|
||||
settingsLink.click();
|
||||
|
||||
// Wait for settings to load
|
||||
await page.waitForSelector("text=Settings");
|
||||
await loadHome(page);
|
||||
await visitSettings(page);
|
||||
|
||||
// Click the "Backup" link
|
||||
await page.click("text=Backup");
|
||||
@@ -48,6 +34,13 @@ test("test local encrypt", async ({ page }) => {
|
||||
// Click the "I wrote down the words" button
|
||||
await wroteDownButton.click();
|
||||
|
||||
// Make sure the balance box ready light is on
|
||||
await page.locator("title=READY");
|
||||
|
||||
// Go back to settings / change password
|
||||
await visitSettings(page);
|
||||
await page.click("text=Change Password");
|
||||
|
||||
// The header should now say "Encrypt your seed words"
|
||||
await expect(page.locator("h1")).toContainText(["Encrypt your seed words"]);
|
||||
|
||||
@@ -56,7 +49,7 @@ test("test local encrypt", async ({ page }) => {
|
||||
const passwordInput = await page.locator(`input[name='password']`);
|
||||
|
||||
// 2. Type the password into the input field
|
||||
await passwordInput.type("test");
|
||||
await passwordInput.fill("test");
|
||||
|
||||
// 3. Find the input field with the name "confirmPassword"
|
||||
const confirmPasswordInput = await page.locator(
|
||||
@@ -64,15 +57,21 @@ test("test local encrypt", async ({ page }) => {
|
||||
);
|
||||
|
||||
// 4. Type the password into the input field
|
||||
await confirmPasswordInput.type("test");
|
||||
await confirmPasswordInput.fill("test");
|
||||
|
||||
// The "Encrypt" button should not be disabled
|
||||
const encryptButton = await page.locator("button", { hasText: "Encrypt" });
|
||||
await expect(encryptButton).not.toBeDisabled();
|
||||
|
||||
// wait 5 seconds for no reason (SADLY THIS IS IMPORTANT FOR THE TEST TO PASS)
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Click the "Encrypt" button
|
||||
await encryptButton.click();
|
||||
|
||||
// wait for a while just to see what happens
|
||||
// await page.waitForTimeout(10000);
|
||||
|
||||
// Wait for a modal with the text "Enter your password"
|
||||
await page.waitForSelector("text=Enter your password");
|
||||
|
||||
@@ -80,11 +79,11 @@ test("test local encrypt", async ({ page }) => {
|
||||
const passwordInput2 = await page.locator(`input[name='password']`);
|
||||
|
||||
// Type the password into the input field
|
||||
await passwordInput2.type("test");
|
||||
await passwordInput2.fill("test");
|
||||
|
||||
// Click the "Decrypt Wallet" button
|
||||
await page.click("text=Decrypt Wallet");
|
||||
|
||||
// Wait for an element matching the selector to appear in DOM.
|
||||
await page.waitForSelector("text=0 SATS");
|
||||
await page.locator(`text=0 sats`).first();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { loadHome, visitSettings } from "./utils";
|
||||
|
||||
const SIGNET_INVITE_CODE =
|
||||
"fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er";
|
||||
|
||||
@@ -8,24 +10,8 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test("fedmint join, receive, send", async ({ page }) => {
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
|
||||
// Wait for an element matching the selector to appear in DOM.
|
||||
await page.waitForSelector("text=0 SATS");
|
||||
|
||||
console.log("Page loaded.");
|
||||
|
||||
// Wait for a while just to make sure we can load everything
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Navigate to settings
|
||||
const settingsLink = await page.getByRole("link", { name: "Settings" });
|
||||
|
||||
settingsLink.click();
|
||||
|
||||
// Wait for settings to load
|
||||
await page.waitForSelector("text=Settings");
|
||||
await loadHome(page);
|
||||
await visitSettings(page);
|
||||
|
||||
// Click "Manage Federations" link
|
||||
await page.click("text=Manage Federations");
|
||||
@@ -45,11 +31,20 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
await page.goBack();
|
||||
await page.goBack();
|
||||
|
||||
// Make sure there's a fedimint icon
|
||||
await expect(page.getByRole("img", { name: "community" })).toBeVisible();
|
||||
// Click the top left button (it's the profile button), a child of header
|
||||
// TODO: better ARIA stuff
|
||||
await page.locator(`header button`).first().click();
|
||||
|
||||
// Click the receive button
|
||||
await page.click("text=Receive");
|
||||
// Make sure there's text that says "fedimint"
|
||||
await page.locator("text=fedimint").first();
|
||||
|
||||
// Navigate back home
|
||||
await page.goBack();
|
||||
|
||||
// Click the fab button
|
||||
await page.locator("#fab").click();
|
||||
// Click the receive button in the fab
|
||||
await page.locator("text=Receive").last().click();
|
||||
|
||||
// Expect the url to conain receive
|
||||
await expect(page).toHaveURL(/.*receive/);
|
||||
@@ -57,9 +52,6 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
// At least one h1 should show "0 sats"
|
||||
await expect(page.locator("h1")).toContainText(["0 SATS"]);
|
||||
|
||||
// At least one h2 should show "0 USD"
|
||||
await expect(page.locator("h2")).toContainText(["$0 USD"]);
|
||||
|
||||
// Type 100 into the input
|
||||
await page.locator("#sats-input").pressSequentially("100");
|
||||
|
||||
@@ -72,11 +64,7 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
});
|
||||
await expect(continueButton).not.toBeDisabled();
|
||||
|
||||
// Wait one second
|
||||
// TODO: figure out how to not get an error without waiting
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
continueButton.click();
|
||||
await continueButton.click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Keep Mutiny open to complete the payment.")
|
||||
@@ -109,21 +97,17 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
);
|
||||
|
||||
// Wait for an h1 to appear in the dom that says "Payment Received"
|
||||
await page.waitForSelector("text=Payment Received", { timeout: 30000 });
|
||||
await page.waitForSelector("text=Payment Received");
|
||||
|
||||
// Click the "Nice" button
|
||||
await page.click("text=Nice");
|
||||
|
||||
// Make sure we have 100 sats in the fedimint balance
|
||||
await expect(
|
||||
page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^100 eSATS$/ })
|
||||
.nth(1)
|
||||
).toBeVisible();
|
||||
// Make sure we have 100 sats in the top balance
|
||||
await page.waitForSelector("text=100 SATS");
|
||||
|
||||
// Now we send
|
||||
await page.click("text=Send");
|
||||
await page.locator("#fab").click();
|
||||
await page.locator("text=Send").last().click();
|
||||
|
||||
// type refund@lnurl-staging.mutinywallet.com
|
||||
const sendInput = await page.locator("input");
|
||||
@@ -131,9 +115,8 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
|
||||
await page.click("text=Continue");
|
||||
|
||||
// Wait two seconds (the destination doesn't show up immediately)
|
||||
// TODO: figure out how to not get an error without waiting
|
||||
await page.waitForTimeout(2000);
|
||||
// Wait for the destination to show up
|
||||
await page.waitForSelector("text=LIGHTNING");
|
||||
|
||||
// Type 90 into the input
|
||||
await page.locator("#sats-input").fill("90");
|
||||
@@ -147,8 +130,8 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
});
|
||||
await expect(confirmButton).not.toBeDisabled();
|
||||
|
||||
confirmButton.click();
|
||||
await confirmButton.click();
|
||||
|
||||
// Wait for an h1 to appear in the dom that says "Payment Sent"
|
||||
await page.waitForSelector("text=Payment Sent", { timeout: 30000 });
|
||||
await page.waitForSelector("text=Payment Sent");
|
||||
});
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
|
||||
import { loadHome } from "./utils";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("http://localhost:3420/");
|
||||
});
|
||||
|
||||
test("initial load", async ({ page }) => {
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
|
||||
await expect(page.locator("header")).toContainText(["Activity"], {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// Wait up to 30 seconds for an image element matching the selector to be visible
|
||||
await page.waitForSelector("img[alt='lightning']", { timeout: 30000 });
|
||||
|
||||
// Wait for an element matching the selector to appear in DOM.
|
||||
await page.waitForSelector("text=0 SATS");
|
||||
|
||||
console.log("Page loaded.");
|
||||
await loadHome(page);
|
||||
});
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { visitSettings } from "./utils";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("http://localhost:3420/");
|
||||
});
|
||||
|
||||
test("restore from seed @slow", async ({ page }) => {
|
||||
// Start on the home page
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
await page.waitForSelector("text=Welcome to the Mutiny!");
|
||||
|
||||
console.log("Waiting for new wallet to be created...");
|
||||
|
||||
await page.locator(`button:has-text('Import Existing')`).click();
|
||||
|
||||
// should have 100k sats on-chain
|
||||
const TEST_SEED_WORDS =
|
||||
"rival hood review write spoon tide orange ill opera enrich clip acoustic";
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
|
||||
// Wait for an element matching the selector to appear in DOM.
|
||||
await page.waitForSelector("text=0 SATS");
|
||||
|
||||
console.log("Page loaded.");
|
||||
|
||||
// Wait for a while just to make sure we can load everything
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Navigate to settings
|
||||
const settingsLink = await page.getByRole("link", { name: "Settings" });
|
||||
|
||||
settingsLink.click();
|
||||
|
||||
// Wait for settings to load
|
||||
await page.waitForSelector("text=Settings");
|
||||
|
||||
// Click the "Restore" link
|
||||
page.click("text=Restore");
|
||||
|
||||
// There should be some warning text: "This will replace your existing wallet"
|
||||
await expect(page.locator("p")).toContainText([
|
||||
"This will replace your existing wallet"
|
||||
]);
|
||||
|
||||
let seedWords = TEST_SEED_WORDS.split(" ");
|
||||
const seedWords = TEST_SEED_WORDS.split(" ");
|
||||
|
||||
// Find the input field with the name "words.0"
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const wordInput = await page.locator(`input[name='words.${i}']`);
|
||||
|
||||
// Type the seed words into the input field
|
||||
await wordInput.type(seedWords[i]);
|
||||
await wordInput.fill(seedWords[i]);
|
||||
}
|
||||
|
||||
// There should be a button with the text "Restore" and it should not be disabled
|
||||
@@ -54,33 +42,29 @@ test("restore from seed @slow", async ({ page }) => {
|
||||
|
||||
// A modal should pop up, click the "Confirm" button
|
||||
const confirmButton = await page.locator("button", { hasText: "Confirm" });
|
||||
confirmButton.click();
|
||||
|
||||
// Wait for the wallet to load
|
||||
await page.waitForSelector("img[alt='lightning']");
|
||||
await confirmButton.click();
|
||||
|
||||
// Eventually we should have a balance of 100k sats
|
||||
await page.waitForSelector("text=100,000 SATS");
|
||||
await page.locator("text=100,000 SATS");
|
||||
|
||||
// Now we should clean up after ourselves and delete the wallet
|
||||
settingsLink.click();
|
||||
|
||||
// Wait for settings to load
|
||||
await page.waitForSelector("text=Settings");
|
||||
await visitSettings(page);
|
||||
|
||||
// Click the "Restore" link
|
||||
page.click("text=Admin Page");
|
||||
await page.click("text=Admin Page");
|
||||
|
||||
// Clicke the Delete Everything button
|
||||
page.click("text=Delete Everything");
|
||||
await page.click("text=Delete Everything");
|
||||
|
||||
// A modal should pop up, click the "Confirm" button
|
||||
const confirmDeleteButton = await page.locator("button", {
|
||||
hasText: "Confirm"
|
||||
});
|
||||
confirmDeleteButton.click();
|
||||
|
||||
// Wait for the wallet to load
|
||||
// Wait for the wallet to load
|
||||
await page.waitForSelector("img[alt='lightning']");
|
||||
// wait 5 seconds for no reason
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await confirmDeleteButton.click();
|
||||
|
||||
await page.locator("text=Welcome to the Mutiny!");
|
||||
});
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { loadHome } from "./utils";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("http://localhost:3420/");
|
||||
});
|
||||
|
||||
test("rountrip receive and send", async ({ page }) => {
|
||||
// Click the receive button
|
||||
await page.click("text=Receive");
|
||||
await loadHome(page);
|
||||
|
||||
// Expect the url to conain receive
|
||||
await page.locator("#fab").click();
|
||||
await page.locator("text=Receive").last().click();
|
||||
|
||||
// Expect the url to contain receive
|
||||
await expect(page).toHaveURL(/.*receive/);
|
||||
|
||||
// At least one h1 should show "0 sats"
|
||||
await expect(page.locator("h1")).toContainText(["0 SATS"]);
|
||||
|
||||
// At least one h2 should show "0 USD"
|
||||
await expect(page.locator("h2")).toContainText(["$0 USD"]);
|
||||
// await expect(page.locator("h2")).toContainText(["$0 USD"]);
|
||||
await page.waitForSelector("text=$0 USD");
|
||||
|
||||
// Type 100000 into the input
|
||||
await page.locator("#sats-input").pressSequentially("100000");
|
||||
@@ -72,7 +77,8 @@ test("rountrip receive and send", async ({ page }) => {
|
||||
await page.click("text=Nice");
|
||||
|
||||
// Now we send
|
||||
await page.click("text=Send");
|
||||
await page.locator("#fab").click();
|
||||
await page.locator("text=Send").click();
|
||||
|
||||
// In the textarea with the placeholder "bitcoin:..." type refund@lnurl-staging.mutinywallet.com
|
||||
const sendInput = await page.locator("input");
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { expect, Page, test } from "@playwright/test";
|
||||
|
||||
import { loadHome, visitSettings } from "./utils";
|
||||
|
||||
const routes = [
|
||||
"/",
|
||||
"/activity",
|
||||
"/feedback",
|
||||
"/gift",
|
||||
"/receive",
|
||||
"/scanner",
|
||||
"/search",
|
||||
"/send",
|
||||
"/swap",
|
||||
"/settings"
|
||||
@@ -57,25 +59,13 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test("visit each route", async ({ page }) => {
|
||||
// Start on the home page
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
|
||||
// Wait for an element matching the selector to appear in DOM.
|
||||
await page.waitForSelector("text=0 SATS");
|
||||
|
||||
console.log("Page loaded.");
|
||||
|
||||
// Wait for a while just to make sure we can load everything
|
||||
await page.waitForTimeout(1000);
|
||||
await loadHome(page);
|
||||
|
||||
checklist.set("/", true);
|
||||
|
||||
await checkRoute(page, "/activity", "Activity", checklist);
|
||||
await page.goBack();
|
||||
await visitSettings(page);
|
||||
|
||||
// Navigate to settings
|
||||
await checkRoute(page, "/settings", "Settings", checklist);
|
||||
checklist.set("/settings", true);
|
||||
|
||||
// Mutiny+
|
||||
await checkRoute(page, "/settings/plus", "Mutiny+", checklist);
|
||||
@@ -146,22 +136,43 @@ test("visit each route", async ({ page }) => {
|
||||
await checkRoute(page, "/settings/admin", "Secret Debug Tools", checklist);
|
||||
await page.goBack();
|
||||
|
||||
// Go back home
|
||||
await page.goBack();
|
||||
|
||||
// Feedback
|
||||
await checkRoute(page, "/feedback", "Give us feedback!", checklist);
|
||||
await page.goBack();
|
||||
|
||||
// Receive is covered in another test
|
||||
checklist.set("/receive", true);
|
||||
// Go back home
|
||||
await page.goBack();
|
||||
|
||||
// Try the fab button
|
||||
await page.locator("#fab").click();
|
||||
await page.locator("text=Send").click();
|
||||
await expect(page.locator("input").first()).toBeFocused();
|
||||
|
||||
// Send is covered in another test
|
||||
checklist.set("/send", true);
|
||||
|
||||
await page.goBack();
|
||||
|
||||
// Try the fab button again
|
||||
await page.locator("#fab").click();
|
||||
// (There are actually two buttons with the "Receive text on first run)
|
||||
await page.locator("text=Receive").last().click();
|
||||
|
||||
await expect(page.locator("h1").first()).toHaveText("Receive Bitcoin");
|
||||
|
||||
// Actual receive is covered in another test
|
||||
checklist.set("/receive", true);
|
||||
|
||||
await page.goBack();
|
||||
|
||||
// Try the fab button again
|
||||
await page.locator("#fab").click();
|
||||
await page.locator("text=Scan").click();
|
||||
|
||||
// Scanner
|
||||
await page.locator(`a[href='/scanner']`).first().click();
|
||||
await expect(page.locator("button").first()).toHaveText("Paste Something");
|
||||
await expect(
|
||||
page.locator("button:has-text('Paste Something')")
|
||||
).toBeVisible();
|
||||
checklist.set("/scanner", true);
|
||||
|
||||
// Now we have to check routes that aren't linked to directly for whatever reason
|
||||
|
||||
27
e2e/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect, Page } from "@playwright/test";
|
||||
|
||||
export async function loadHome(page: Page) {
|
||||
// Start on the home page
|
||||
await expect(page).toHaveTitle(/Mutiny Wallet/);
|
||||
await page.waitForSelector("text=Welcome to the Mutiny!");
|
||||
|
||||
console.log("Waiting for new wallet to be created...");
|
||||
|
||||
await page.locator(`button:has-text('New Wallet')`).click();
|
||||
|
||||
await page.locator("text=Create your profile").first();
|
||||
|
||||
await page.locator("button:has-text('Skip for now')").click();
|
||||
|
||||
// Should have a balance up top now
|
||||
await page.locator(`text=0 sats`).first();
|
||||
// Status light should be ready
|
||||
await page.locator(`title="READY"`).first();
|
||||
}
|
||||
|
||||
export async function visitSettings(page: Page) {
|
||||
// Find an image with an alt text of "mutiny" and click it
|
||||
// TODO: probably should have better ARIA stuff for this
|
||||
await page.locator("img[alt='mutiny']").first().click();
|
||||
await expect(page.locator("h1").first()).toHaveText("Settings");
|
||||
}
|
||||
11
index.html
@@ -47,6 +47,9 @@
|
||||
#no-script {
|
||||
margin: 1rem;
|
||||
}
|
||||
body {
|
||||
background-color: hsla(0, 0%, 5%, 1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -63,8 +66,10 @@
|
||||
Please update or enable WebAssembly to run this app.
|
||||
</p>
|
||||
<p>
|
||||
If you're running iOS in lockdown mode you'll need to <a href="https://support.apple.com/en-us/HT212650">add an
|
||||
exception for Mutiny Wallet.</a>
|
||||
If you're running iOS in lockdown mode you'll need to
|
||||
<a href="https://support.apple.com/en-us/HT212650"
|
||||
>add an exception for Mutiny Wallet.</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<noscript>
|
||||
@@ -82,7 +87,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<script>
|
||||
// Check for WebAssembly support
|
||||
|
||||
@@ -14,12 +14,14 @@ def capacitor_pods
|
||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.3.0_@capacitor+core@5.5.1/node_modules/@capacitor-mlkit/barcode-scanning'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app'
|
||||
pod 'CapacitorAppLauncher', :path => '../../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera'
|
||||
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@5.1.4_@capacitor+core@5.5.1/node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/haptics'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/status-bar'
|
||||
pod 'CapacitorToast', :path => '../../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast'
|
||||
pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
||||
@@ -5,6 +5,8 @@ PODS:
|
||||
- Capacitor
|
||||
- CapacitorAppLauncher (5.0.6):
|
||||
- Capacitor
|
||||
- CapacitorCamera (5.0.9):
|
||||
- Capacitor
|
||||
- CapacitorClipboard (5.0.6):
|
||||
- Capacitor
|
||||
- CapacitorCordova (5.5.1)
|
||||
@@ -15,6 +17,9 @@ PODS:
|
||||
- CapacitorMlkitBarcodeScanning (5.3.0):
|
||||
- Capacitor
|
||||
- GoogleMLKit/BarcodeScanning (= 4.0.0)
|
||||
- CapacitorSecureStoragePlugin (0.9.0):
|
||||
- Capacitor
|
||||
- SwiftKeychainWrapper
|
||||
- CapacitorShare (5.0.6):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (5.0.6):
|
||||
@@ -75,16 +80,19 @@ PODS:
|
||||
- nanopb/decode (2.30909.0)
|
||||
- nanopb/encode (2.30909.0)
|
||||
- PromisesObjC (2.3.1)
|
||||
- SwiftKeychainWrapper (4.0.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/.pnpm/@capacitor+ios@5.5.1_@capacitor+core@5.5.1/node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app`)"
|
||||
- "CapacitorAppLauncher (from `../../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher`)"
|
||||
- "CapacitorCamera (from `../../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera`)"
|
||||
- "CapacitorClipboard (from `../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard`)"
|
||||
- "CapacitorCordova (from `../../node_modules/.pnpm/@capacitor+ios@5.5.1_@capacitor+core@5.5.1/node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/.pnpm/@capacitor+filesystem@5.1.4_@capacitor+core@5.5.1/node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorHaptics (from `../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/haptics`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.3.0_@capacitor+core@5.5.1/node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
- "CapacitorSecureStoragePlugin (from `../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin`)"
|
||||
- "CapacitorShare (from `../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/.pnpm/@capacitor+status-bar@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/status-bar`)"
|
||||
- "CapacitorToast (from `../../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast`)"
|
||||
@@ -103,6 +111,7 @@ SPEC REPOS:
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- SwiftKeychainWrapper
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
@@ -111,6 +120,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app"
|
||||
CapacitorAppLauncher:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera"
|
||||
CapacitorClipboard:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard"
|
||||
CapacitorCordova:
|
||||
@@ -121,6 +132,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/haptics"
|
||||
CapacitorMlkitBarcodeScanning:
|
||||
:path: "../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.3.0_@capacitor+core@5.5.1/node_modules/@capacitor-mlkit/barcode-scanning"
|
||||
CapacitorSecureStoragePlugin:
|
||||
:path: "../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share"
|
||||
CapacitorStatusBar:
|
||||
@@ -132,11 +145,13 @@ SPEC CHECKSUMS:
|
||||
Capacitor: 9da0a2415e3b6098511f8b5ffdb578d91ee79f8f
|
||||
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
|
||||
CapacitorAppLauncher: 5a9f06c13c6b4f5d65b550a07128ef04f3a216b3
|
||||
CapacitorCamera: 4892c5c392f60039d853dde78bc50ba19fbd113e
|
||||
CapacitorClipboard: 77edf49827ea21da2a9c05c690a4a6a4d07199c4
|
||||
CapacitorCordova: e128cc7688c070ca0bfa439898a5f609da8dbcfe
|
||||
CapacitorFilesystem: af704badfbc69f6f8623d9ed313e5490e3723dcb
|
||||
CapacitorHaptics: 1fffc1217c7e64a472d7845be50fb0c2f7d4204c
|
||||
CapacitorMlkitBarcodeScanning: ea08ef246e5d3511d5a231a59fae36b16ad9acb3
|
||||
CapacitorSecureStoragePlugin: e91d7df060f2495a1acff9583641a6953e3aacba
|
||||
CapacitorShare: cd41743331cb71d217c029de54b681cbd91e0fcc
|
||||
CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309
|
||||
CapacitorToast: bb0d79b78d9c27c0199b57f735dd50b8fc363489
|
||||
@@ -152,7 +167,8 @@ SPEC CHECKSUMS:
|
||||
MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49
|
||||
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
|
||||
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
|
||||
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
|
||||
|
||||
PODFILE CHECKSUM: 0a4556c3ff620e7baf2e9aeba1f6e9f4fe406a0f
|
||||
PODFILE CHECKSUM: 49918019bef63fe6c45a1c0a4748457e705212b5
|
||||
|
||||
COCOAPODS: 1.14.2
|
||||
COCOAPODS: 1.14.3
|
||||
|
||||
50
package.json
@@ -19,25 +19,23 @@
|
||||
"@capacitor/cli": "^5.5.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@types/node": "^20.8.10",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@typescript-eslint/parser": "^7.0.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"esbuild": "^0.14.54",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-import-resolver-typescript": "2.7.1",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-solid": "0.13.0",
|
||||
"postcss": "^8.4.31",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-solid": "0.13.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.12",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.1",
|
||||
"vite-plugin-pwa": "^0.16.7",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
"vite-plugin-wasm": "^3.2.2",
|
||||
"vite-plugin-solid": "^2.10.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -54,19 +52,21 @@
|
||||
"@capacitor/share": "^5.0.6",
|
||||
"@capacitor/status-bar": "^5.0.6",
|
||||
"@capacitor/toast": "^5.0.6",
|
||||
"@kobalte/core": "^0.9.8",
|
||||
"@kobalte/tailwindcss": "^0.5.0",
|
||||
"@modular-forms/solid": "^0.18.1",
|
||||
"@mutinywallet/mutiny-wasm": "0.5.10",
|
||||
"@mutinywallet/waila-wasm": "^0.2.6",
|
||||
"@kobalte/core": "^0.12.1",
|
||||
"@kobalte/tailwindcss": "^0.9.0",
|
||||
"@mutinywallet/mutiny-wasm": "0.6.0-rc3",
|
||||
"@modular-forms/solid": "^0.20.0",
|
||||
"@solid-primitives/upload": "^0.0.111",
|
||||
"@solidjs/meta": "^0.29.1",
|
||||
"@solidjs/router": "^0.9.0",
|
||||
"@solidjs/meta": "^0.29.3",
|
||||
"@solidjs/router": "^0.10.9",
|
||||
"capacitor-secure-storage-plugin": "^0.9.0",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"lucide-solid": "^0.330.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"solid-js": "^1.8.5",
|
||||
"solid-qr-code": "^0.0.8"
|
||||
"solid-js": "^1.8.12",
|
||||
"solid-qr-code": "^0.0.8",
|
||||
"solid-transition-group": "^0.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
8630
pnpm-lock.yaml
generated
7
postcss.config.cjs
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,9 +33,16 @@
|
||||
"find": "Find your friends on nostr",
|
||||
"backup": "Secure your funds!",
|
||||
"connection": "Create a wallet connection",
|
||||
"federation": "Join a federation"
|
||||
"connection_edit": "Edit wallet connections",
|
||||
"federation": "Join a federation",
|
||||
"subnav": {
|
||||
"just_me": "Just Me",
|
||||
"friends": "Friends",
|
||||
"requests": "Requests"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"profile": "Profile",
|
||||
"nostr_identity": "Nostr Identity",
|
||||
"add_lightning_address": "Add Lightning Address",
|
||||
"edit_profile": "Edit Profile"
|
||||
@@ -465,7 +472,8 @@
|
||||
"ios_testflight": "iOS TestFlight access",
|
||||
"more": "... and more to come",
|
||||
"cta_description": "Enjoy early access to new features and premium functionality.",
|
||||
"cta_but_already_plus": "Thank you for your support!"
|
||||
"cta_but_already_plus": "Thank you for your support!",
|
||||
"lightning_address": "Your own lightning address"
|
||||
},
|
||||
"restore": {
|
||||
"title": "Restore",
|
||||
@@ -525,7 +533,7 @@
|
||||
"remove": "Remove",
|
||||
"expires": "Expires",
|
||||
"federation_id": "Federation ID",
|
||||
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. Store funds in a federation at your own risk!",
|
||||
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. These funds are currently not backed up remotely. Store funds in a federation at your own risk!",
|
||||
"learn_more": "Learn more about Fedimint."
|
||||
},
|
||||
"gift": {
|
||||
|
||||
BIN
public/mutiny-pixel-m-white.png
Normal file
|
After Width: | Height: | Size: 267 B |
BIN
public/mutiny-pixel-m.png
Normal file
|
After Width: | Height: | Size: 281 B |
36
src/App.tsx
@@ -1,36 +0,0 @@
|
||||
// @refresh reload
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { StatusBar, Style } from "@capacitor/status-bar";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { ErrorBoundary, Suspense } from "solid-js";
|
||||
|
||||
import { ErrorDisplay, I18nProvider } from "~/components";
|
||||
import { Router } from "~/router";
|
||||
import { Provider as MegaStoreProvider } from "~/state/megaStore";
|
||||
|
||||
const setStatusBarStyleDark = async () => {
|
||||
await StatusBar.setStyle({ style: Style.Dark });
|
||||
};
|
||||
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
await setStatusBarStyleDark();
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Title>Mutiny Wallet</Title>
|
||||
<ErrorBoundary fallback={(e) => <ErrorDisplay error={e} />}>
|
||||
<MegaStoreProvider>
|
||||
<I18nProvider>
|
||||
<ErrorBoundary
|
||||
fallback={(e) => <ErrorDisplay error={e} />}
|
||||
>
|
||||
<Router />
|
||||
</ErrorBoundary>
|
||||
</I18nProvider>
|
||||
</MegaStoreProvider>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
BIN
src/assets/generic-avatar.jpg
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.597 3.477a1.5 1.5 0 0 1 .278 1.622l-11.768 27a1.5 1.5 0 0 1-2.387.509L14.157 25.7l-3.036 4.578a1.5 1.5 0 0 1-2.73-.58L6.825 20.34.872 17.596a1.5 1.5 0 0 1 .125-2.775l33-11.734a1.5 1.5 0 0 1 1.6.39ZM9.827 20.1l.896 5.35 1.902-2.869a1.5 1.5 0 0 1 .236-.276l11.04-10.122L9.826 20.1Zm8.665-8.317-13.02 4.63 2.633 1.213 10.387-5.843Zm11.805-1.395-14.2 13.02 6.098 5.57 8.102-18.59Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 531 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.546 8 8 17.546l9.546 9.546" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 206 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75 27.75v-13.5a1.5 1.5 0 0 1 3 0v9.879L26.281 7.597a1.5 1.5 0 0 1 2.122 2.122L11.87 26.25h9.879a1.5 1.5 0 0 1 0 3H8.25a1.5 1.5 0 0 1-1.5-1.5Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 294 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14 14 20 20m-20 0 20-20" stroke="#000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 200 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 921 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 921 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#000"/>
|
||||
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#fff"/>
|
||||
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 425 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14 14 20 20m-20 0 20-20" stroke="#FFF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 200 B |
@@ -1,18 +0,0 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<mask id="b" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M.667 8A7.333 7.333 0 0 1 8 .667c.368 0 .667.298.667.666v13.334a.667.667 0 0 1-.667.666A7.333 7.333 0 0 1 .667 8Zm6.666-5.963a6 6 0 0 0 0 11.926V2.037Z" fill="#fff"/>
|
||||
<path d="M8 1.333a6.667 6.667 0 1 1 0 13.334V1.333Z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.333 1.333c0-.368.299-.666.667-.666a7.333 7.333 0 1 1 0 14.666.667.667 0 0 1-.667-.666V1.333Zm1.334.704v11.926a6 6 0 0 0 0-11.926Z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.333 4c0-.368.299-.667.667-.667h5a.667.667 0 0 1 0 1.334H3A.667.667 0 0 1 2.333 4ZM1 6.667C1 6.299 1.298 6 1.667 6H8a.667.667 0 0 1 0 1.333H1.667A.667.667 0 0 1 1 6.667Zm0 2.666c0-.368.298-.666.667-.666H8A.667.667 0 1 1 8 10H1.667A.667.667 0 0 1 1 9.333ZM2.333 12c0-.368.299-.667.667-.667h5a.667.667 0 0 1 0 1.334H3A.667.667 0 0 1 2.333 12Z" fill="#fff"/>
|
||||
</mask>
|
||||
<g mask="url(#b)">
|
||||
<path d="M0 0h16v16H0V0Z" fill="#fff"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h16v16H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512">
|
||||
<path fill="white" d="M12,17a4,4,0,1,1,4-4A4,4,0,0,1,12,17Zm6,4a3,3,0,0,0-3-3H9a3,3,0,0,0-3,3v3H18ZM18,8a4,4,0,1,1,4-4A4,4,0,0,1,18,8ZM6,8a4,4,0,1,1,4-4A4,4,0,0,1,6,8Zm0,5A5.968,5.968,0,0,1,7.537,9H3a3,3,0,0,0-3,3v3H6.349A5.971,5.971,0,0,1,6,13Zm11.651,2H24V12a3,3,0,0,0-3-3H16.463a5.952,5.952,0,0,1,1.188,6Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 481 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 21H8V7h11m0-2H8c-.53043 0-1.03914.21071-1.41421.58579C6.21071 5.96086 6 6.46957 6 7v14c0 .5304.21071 1.0391.58579 1.4142C6.96086 22.7893 7.46957 23 8 23h11c.5304 0 1.0391-.2107 1.4142-.5858S21 21.5304 21 21V7c0-.53043-.2107-1.03914-.5858-1.41421C20.0391 5.21071 19.5304 5 19 5Zm-3-4H4c-.53043 0-1.03914.21071-1.41421.58579C2.21071 1.96086 2 2.46957 2 3v14h2V3h12V1Z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 478 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 21H8V7h11m0-2H8c-.53043 0-1.03914.21071-1.41421.58579C6.21071 5.96086 6 6.46957 6 7v14c0 .5304.21071 1.0391.58579 1.4142C6.96086 22.7893 7.46957 23 8 23h11c.5304 0 1.0391-.2107 1.4142-.5858S21 21.5304 21 21V7c0-.53043-.2107-1.03914-.5858-1.41421C20.0391 5.21071 19.5304 5 19 5Zm-3-4H4c-.53043 0-1.03914.21071-1.41421.58579C2.21071 1.96086 2 2.46957 2 3v14h2V3h12V1Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 478 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.5 3.99992L4.16667 1.33325M4.16667 1.33325L6.83333 3.99992M4.16667 1.33325V10.6666M13.5 7.99992L10.8333 10.6666M10.8333 10.6666L8.16667 7.99992M10.8333 10.6666V1.33325" stroke="#A3A3A3" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 372 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icons">
|
||||
<path id="Vector" d="M7 10L12 15L17 10" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 250 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6ZM12 16L7 11L8.4 9.55L11 12.15V4H13V12.15L15.6 9.55L17 11L12 16Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 359 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5ZM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 318 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.667 1.66406H3.33366C2.41699 1.66406 1.67533 2.41406 1.67533 3.33073L1.66699 18.3307L5.00033 14.9974H16.667C17.5837 14.9974 18.3337 14.2474 18.3337 13.3307V3.33073C18.3337 2.41406 17.5837 1.66406 16.667 1.66406ZM5.83366 7.4974H14.167C14.6253 7.4974 15.0003 7.8724 15.0003 8.33073C15.0003 8.78906 14.6253 9.16406 14.167 9.16406H5.83366C5.37533 9.16406 5.00033 8.78906 5.00033 8.33073C5.00033 7.8724 5.37533 7.4974 5.83366 7.4974ZM10.8337 11.6641H5.83366C5.37533 11.6641 5.00033 11.2891 5.00033 10.8307C5.00033 10.3724 5.37533 9.9974 5.83366 9.9974H10.8337C11.292 9.9974 11.667 10.3724 11.667 10.8307C11.667 11.2891 11.292 11.6641 10.8337 11.6641ZM14.167 6.66406H5.83366C5.37533 6.66406 5.00033 6.28906 5.00033 5.83073C5.00033 5.3724 5.37533 4.9974 5.83366 4.9974H14.167C14.6253 4.9974 15.0003 5.3724 15.0003 5.83073C15.0003 6.28906 14.6253 6.66406 14.167 6.66406Z" fill="#B9B9B9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 996 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icons">
|
||||
<path id="Vector" d="M10 17L15 12L10 7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 244 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.75 20.9531C3.75 21.368 4.08516 21.7031 4.5 21.7031H11.2031V12.8906H3.75V20.9531ZM12.7969 21.7031H19.5C19.9148 21.7031 20.25 21.368 20.25 20.9531V12.8906H12.7969V21.7031ZM20.625 7.26562H17.1656C17.4844 6.76406 17.6719 6.16875 17.6719 5.53125C17.6719 3.74766 16.2211 2.29688 14.4375 2.29688C13.4672 2.29688 12.593 2.72812 12 3.40781C11.407 2.72812 10.5328 2.29688 9.5625 2.29688C7.77891 2.29688 6.32812 3.74766 6.32812 5.53125C6.32812 6.16875 6.51328 6.76406 6.83438 7.26562H3.375C2.96016 7.26562 2.625 7.60078 2.625 8.01562V11.2969H11.2031V7.26562H12.7969V11.2969H21.375V8.01562C21.375 7.60078 21.0398 7.26562 20.625 7.26562ZM11.2031 7.17188H9.5625C8.65781 7.17188 7.92188 6.43594 7.92188 5.53125C7.92188 4.62656 8.65781 3.89062 9.5625 3.89062C10.4672 3.89062 11.2031 4.62656 11.2031 5.53125V7.17188ZM14.4375 7.17188H12.7969V5.53125C12.7969 4.62656 13.5328 3.89062 14.4375 3.89062C15.3422 3.89062 16.0781 4.62656 16.0781 5.53125C16.0781 6.43594 15.3422 7.17188 14.4375 7.17188Z" fill="#FA0050"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="hsla(163, 70%, 38%, 1)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 443 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icons">
|
||||
<path id="Vector" d="M10.0463 7.50016L9.44634 8.1135C8.96634 8.5935 8.66634 9.00016 8.66634 10.0002H7.33301V9.66683C7.33301 8.92683 7.63301 8.26016 8.11301 7.78016L8.93967 6.94016C9.18634 6.70016 9.33301 6.36683 9.33301 6.00016C9.33301 5.64654 9.19253 5.3074 8.94248 5.05735C8.69243 4.8073 8.3533 4.66683 7.99967 4.66683C7.64605 4.66683 7.30691 4.8073 7.05687 5.05735C6.80682 5.3074 6.66634 5.64654 6.66634 6.00016H5.33301C5.33301 5.29292 5.61396 4.61464 6.11406 4.11454C6.61415 3.61445 7.29243 3.3335 7.99967 3.3335C8.70692 3.3335 9.38519 3.61445 9.88529 4.11454C10.3854 4.61464 10.6663 5.29292 10.6663 6.00016C10.6654 6.56232 10.4426 7.10138 10.0463 7.50016ZM8.66634 12.6668H7.33301V11.3335H8.66634M7.99967 1.3335C7.1242 1.3335 6.25729 1.50593 5.44845 1.84097C4.63961 2.176 3.90469 2.66706 3.28563 3.28612C2.03539 4.53636 1.33301 6.23205 1.33301 8.00016C1.33301 9.76827 2.03539 11.464 3.28563 12.7142C3.90469 13.3333 4.63961 13.8243 5.44845 14.1594C6.25729 14.4944 7.1242 14.6668 7.99967 14.6668C9.76778 14.6668 11.4635 13.9644 12.7137 12.7142C13.964 11.464 14.6663 9.76827 14.6663 8.00016C14.6663 4.3135 11.6663 1.3335 7.99967 1.3335Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 17h2v-6h-2v6Zm1-8c.2833 0 .521-.096.713-.288.192-.192.2877-.42933.287-.712 0-.28333-.096-.521-.288-.713-.192-.192-.4293-.28767-.712-.287-.2833 0-.521.096-.713.288-.192.192-.2877.42933-.287.712 0 .28333.096.521.288.713.192.192.4293.28767.712.287Zm0 13c-1.3833 0-2.68333-.2627-3.9-.788-1.21667-.5253-2.275-1.2377-3.175-2.137-.9-.9-1.61233-1.9583-2.137-3.175S2.00067 13.3833 2 12c0-1.3833.26267-2.68333.788-3.9.52533-1.21667 1.23767-2.275 2.137-3.175.9-.9 1.95833-1.61233 3.175-2.137S10.6167 2.00067 12 2c1.3833 0 2.6833.26267 3.9.788 1.2167.52533 2.275 1.23767 3.175 2.137.9.9 1.6127 1.95833 2.138 3.175.5253 1.21667.7877 2.5167.787 3.9 0 1.3833-.2627 2.6833-.788 3.9-.5253 1.2167-1.2377 2.275-2.137 3.175-.9.9-1.9583 1.6127-3.175 2.138-1.2167.5253-2.5167.7877-3.9.787Zm0-2c2.2333 0 4.125-.775 5.675-2.325C19.225 16.125 20 14.2333 20 12c0-2.23333-.775-4.125-2.325-5.675C16.125 4.775 14.2333 4 12 4c-2.23333 0-4.125.775-5.675 2.325C4.775 7.875 4 9.76667 4 12c0 2.2333.775 4.125 2.325 5.675C7.875 19.225 9.76667 20 12 20Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 28.4H5.08333V29.7H8.95833V28.4H10.25V15.4H11.5417V12.8H10.25V11.5H7.66667V12.8H6.375V14.1H5.08333V15.4H8.95833V18H7.66667V19.3H5.08333V18H3.79167V12.8H5.08333V11.5H6.375V10.2H14.125V11.5H15.4167V12.8H16.7083V15.4H18V18H19.2917V19.3H20.5833V16.7H21.875V15.4H23.1667V12.8H24.4583V10.2H25.75V8.9H27.0417V6.3H29.625V5H32.2083V6.3H33.5V7.6H30.9167V6.3H29.625V7.6H28.3333V23.2H29.625V29.7H30.9167V31H25.75V29.7H24.4583V16.7H23.1667V18H21.875V20.6H20.5833V23.2H19.2917V25.8H16.7083V23.2H15.4167V20.6H14.125V16.7H12.8333V24.5H11.5417V28.4H10.25V29.7H8.95833V31H3.79167V29.7H2.5V28.4Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 709 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z" fill="#fff"/>
|
||||
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 569 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.707 19.7069 18 10.4139l-4.414-4.41402-9.293 9.29302c-.12794.1281-.21882.2884-.263.464L3 20.9999l5.242-1.03c.176-.044.337-.135.465-.263ZM21 7.41388c.3749-.37506.5856-.88367.5856-1.414s-.2107-1.03895-.5856-1.414l-1.586-1.586c-.3751-.37494-.8837-.58557-1.414-.58557-.5303 0-1.0389.21063-1.414.58557L15 4.58588l4.414 4.414L21 7.41388Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 442 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M9.8585 7.5L12.5002 10.1333V10C12.5002 9.33696 12.2368 8.70107 11.7679 8.23223C11.2991 7.76339 10.6632 7.5 10.0002 7.5H9.8585ZM6.27516 8.16667L7.56683 9.45833C7.52516 9.63333 7.50016 9.80833 7.50016 10C7.50016 10.663 7.76356 11.2989 8.2324 11.7678C8.70124 12.2366 9.33712 12.5 10.0002 12.5C10.1835 12.5 10.3668 12.475 10.5418 12.4333L11.8335 13.725C11.2752 14 10.6585 14.1667 10.0002 14.1667C8.89509 14.1667 7.83529 13.7277 7.05388 12.9463C6.27248 12.1649 5.8335 11.1051 5.8335 10C5.8335 9.34167 6.00016 8.725 6.27516 8.16667ZM1.66683 3.55833L3.56683 5.45833L3.94183 5.83333C2.56683 6.91667 1.4835 8.33333 0.833496 10C2.27516 13.6583 5.8335 16.25 10.0002 16.25C11.2918 16.25 12.5252 16 13.6502 15.55L14.0085 15.9L16.4418 18.3333L17.5002 17.275L2.72516 2.5M10.0002 5.83333C11.1052 5.83333 12.165 6.27232 12.9464 7.05372C13.7278 7.83512 14.1668 8.89493 14.1668 10C14.1668 10.5333 14.0585 11.05 13.8668 11.5167L16.3085 13.9583C17.5585 12.9167 18.5585 11.55 19.1668 10C17.7252 6.34167 14.1668 3.75 10.0002 3.75C8.8335 3.75 7.71683 3.95833 6.66683 4.33333L8.47516 6.125C8.95016 5.94167 9.4585 5.83333 10.0002 5.83333Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.333 13.008a.667.667 0 0 1-.666.667h-6A.667.667 0 0 1 3 13.008v-6a.667.667 0 1 1 1.333 0v4.39l7.348-7.346a.667.667 0 1 1 .942.942l-7.347 7.348h4.39c.369 0 .667.298.667.666Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 325 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14 14 20 20m-20 0 20-20" stroke="hsla(343, 92%, 54%, 1)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 218 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.6493 6.34989C16.8111 5.50416 15.7937 4.8575 14.6721 4.45768C13.5505 4.05786 12.3534 3.91508 11.1693 4.03989C7.49929 4.40989 4.47929 7.38989 4.06929 11.0599C3.51929 15.9099 7.26929 19.9999 11.9993 19.9999C13.5094 19.9999 14.9885 19.5714 16.2648 18.7642C17.5411 17.957 18.5621 16.8043 19.2093 15.4399C19.5293 14.7699 19.0493 13.9999 18.3093 13.9999C17.9393 13.9999 17.5893 14.1999 17.4293 14.5299C16.8487 15.7789 15.8557 16.7899 14.6172 17.3927C13.3788 17.9955 11.9705 18.1534 10.6293 17.8399C8.40929 17.3499 6.61929 15.5399 6.14929 13.3199C5.95172 12.4422 5.95401 11.5312 6.15598 10.6545C6.35796 9.77775 6.75445 8.95764 7.31614 8.25481C7.87783 7.55198 8.59033 6.98442 9.40096 6.59411C10.2116 6.20379 11.0996 6.00071 11.9993 5.99989C13.6593 5.99989 15.1393 6.68989 16.2193 7.77989L14.7093 9.28989C14.0793 9.91989 14.5193 10.9999 15.4093 10.9999H18.9993C19.5493 10.9999 19.9993 10.5499 19.9993 9.99989V6.40989C19.9993 5.51989 18.9193 5.06989 18.2893 5.69989L17.6493 6.34989Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.7469 12.7455C19.9345 12.558 20.0398 12.3036 20.0398 12.0384C20.0398 11.7732 19.9345 11.5188 19.7469 11.3313L14.1254 5.63909C13.9379 5.45156 13.6835 5.3462 13.4183 5.3462C13.1531 5.3462 12.8987 5.45156 12.7112 5.63909C12.5237 5.82663 12.4183 6.08098 12.4183 6.3462C12.4183 6.61142 12.5237 6.86577 12.7112 7.05331L16.6427 10.9848L4.93303 10.999C4.80102 10.9984 4.67021 11.024 4.54814 11.0743C4.42608 11.1246 4.31517 11.1985 4.22183 11.2918C4.12848 11.3852 4.05454 11.4961 4.00427 11.6182C3.954 11.7402 3.9284 11.871 3.92894 12.0031C3.9284 12.1351 3.954 12.2659 4.00427 12.3879C4.05454 12.51 4.12848 12.6209 4.22183 12.7143C4.31517 12.8076 4.42608 12.8815 4.54814 12.9318C4.67021 12.9821 4.80102 13.0077 4.93303 13.0071L16.6569 13.0071L12.7112 16.9528C12.5237 17.1403 12.4183 17.3947 12.4183 17.6599C12.4183 17.9251 12.5237 18.1795 12.7112 18.367C12.8987 18.5546 13.1531 18.6599 13.4183 18.6599C13.6835 18.6599 13.9379 18.5546 14.1254 18.367L19.7469 12.7455Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,16 +0,0 @@
|
||||
<svg width="64" height="57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="b" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="10" width="34" height="38">
|
||||
<path fill="url(#a)" d="M30 10h34v38H30z"/>
|
||||
</mask>
|
||||
<g mask="url(#b)">
|
||||
<path d="M49.294 46.586c8.25 0 13.219-4.008 13.219-10.313v-.023c0-5.273-3.07-8.133-10.102-9.586l-3.656-.75c-4.078-.844-5.93-2.25-5.93-4.64v-.024c0-2.695 2.461-4.547 6.422-4.57 3.797 0 6.399 1.758 6.797 4.71l.047.282h5.79l-.024-.399c-.352-5.789-5.18-9.68-12.563-9.68-7.289 0-12.515 4.032-12.539 9.985v.024c0 5.039 3.281 8.132 9.938 9.515l3.632.75c4.36.914 6.118 2.274 6.118 4.805v.023c0 2.907-2.672 4.805-6.938 4.805-4.242 0-7.219-1.805-7.664-4.71l-.047-.282h-5.789l.024.351c.398 6.07 5.507 9.727 13.265 9.727Z" fill="#F61D5B" fill-opacity=".5"/>
|
||||
</g>
|
||||
<path d="M2.977 46h6.046V33.344h6.54L22.218 46h6.89l-7.382-13.57c3.937-1.43 6.375-5.11 6.375-9.657v-.046c0-6.54-4.407-10.547-11.625-10.547h-13.5V46Zm6.046-17.438V17.078h6.704c3.796 0 6.187 2.156 6.187 5.695v.047c0 3.633-2.25 5.742-6.07 5.742h-6.82Z" fill="#F61D5B"/>
|
||||
<path d="M44.86 46.586c8.25 0 13.218-4.008 13.218-10.313v-.023c0-5.273-3.07-8.133-10.101-9.586l-3.657-.75c-4.078-.844-5.93-2.25-5.93-4.64v-.024c0-2.695 2.462-4.547 6.422-4.57 3.797 0 6.399 1.758 6.797 4.71l.047.282h5.79l-.024-.399c-.352-5.789-5.18-9.68-12.563-9.68-7.289 0-12.515 4.032-12.539 9.985v.024c0 5.039 3.282 8.132 9.938 9.515l3.633.75c4.359.914 6.117 2.274 6.117 4.805v.023c0 2.907-2.672 4.805-6.938 4.805-4.242 0-7.218-1.805-7.664-4.71l-.047-.282H31.57l.024.351c.398 6.07 5.508 9.727 13.265 9.727Z" fill="#F61D5B" fill-opacity=".75"/>
|
||||
<defs>
|
||||
<linearGradient id="a" x1="64" y1="29.292" x2="30" y2="29.292" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#black" stop-opacity=".5"/>
|
||||
<stop offset="1" stop-color="#black"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 3c-.53043 0-1.03914.21071-1.41421.58579C3.21071 3.96086 3 4.46957 3 5v14c0 .5304.21071 1.0391.58579 1.4142C3.96086 20.7893 4.46957 21 5 21h14c.5304 0 1.0391-.2107 1.4142-.5858S21 19.5304 21 19V5.5L18.5 3H17v6c0 .26522-.1054.51957-.2929.70711C16.5196 9.89464 16.2652 10 16 10H8c-.26522 0-.51957-.10536-.70711-.29289C7.10536 9.51957 7 9.26522 7 9V3H5Zm7 1v5h3V4h-3Zm-5 8h10c.2652 0 .5196.1054.7071.2929S18 12.7348 18 13v6H6v-6c0-.2652.10536-.5196.29289-.7071C6.48043 12.1054 6.73478 12 7 12Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 601 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 526 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.66667 4.16667C5.66667 3.79848 5.96515 3.5 6.33334 3.5H12.3333C12.7015 3.5 13 3.79848 13 4.16667V10.1667C13 10.5349 12.7015 10.8333 12.3333 10.8333C11.9651 10.8333 11.6667 10.5349 11.6667 10.1667V5.77614L4.31941 13.1234C4.05906 13.3838 3.63695 13.3838 3.3766 13.1234C3.11625 12.8631 3.11625 12.4409 3.3766 12.1806L10.7239 4.83333H6.33334C5.96515 4.83333 5.66667 4.53486 5.66667 4.16667Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 557 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.25 18C29.25 17.655 29.235 17.325 29.205 16.98L31.995 14.865C32.595 14.415 32.76 13.575 32.385 12.915L29.58 8.07C29.4001 7.75228 29.1092 7.51221 28.7631 7.39593C28.417 7.27965 28.0402 7.29534 27.705 7.44L24.48 8.805C23.925 8.415 23.34 8.07 22.725 7.785L22.29 4.32C22.2 3.57 21.555 3 20.805 3H15.21C14.445 3 13.8 3.57 13.71 4.32L13.275 7.785C12.66 8.07 12.075 8.415 11.52 8.805L8.29499 7.44C7.60499 7.14 6.79499 7.41 6.41999 8.07L3.61499 12.93C3.23999 13.59 3.40499 14.415 4.00499 14.88L6.79499 16.995C6.73281 17.6686 6.73281 18.3464 6.79499 19.02L4.00499 21.135C3.40499 21.585 3.23999 22.425 3.61499 23.085L6.41999 27.93C6.79499 28.59 7.60499 28.86 8.29499 28.56L11.52 27.195C12.075 27.585 12.66 27.93 13.275 28.215L13.71 31.68C13.8 32.43 14.445 33 15.195 33H20.79C21.54 33 22.185 32.43 22.275 31.68L22.71 28.215C23.325 27.93 23.91 27.585 24.465 27.195L27.69 28.56C28.38 28.86 29.19 28.59 29.565 27.93L32.37 23.085C32.745 22.425 32.58 21.6 31.98 21.135L29.19 19.02C29.235 18.675 29.25 18.345 29.25 18ZM18.06 23.25C15.165 23.25 12.81 20.895 12.81 18C12.81 15.105 15.165 12.75 18.06 12.75C20.955 12.75 23.31 15.105 23.31 18C23.31 20.895 20.955 23.25 18.06 23.25Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 23c-.55 0-1.021-.196-1.413-.588C4.195 22.02 3.99934 21.5493 4 21V10c0-.55.196-1.021.588-1.413C4.98 8.195 5.45067 7.99933 6 8h3v2H6v11h12V10h-3V8h3c.55 0 1.021.196 1.413.588.392.392.5877.86267.587 1.412v11c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-7V4.825l-1.6 1.6L8 5l4-4 4 4-1.4 1.425-1.6-1.6V16h-2Z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 433 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 23c-.55 0-1.021-.196-1.413-.588C4.195 22.02 3.99934 21.5493 4 21V10c0-.55.196-1.021.588-1.413C4.98 8.195 5.45067 7.99933 6 8h3v2H6v11h12V10h-3V8h3c.55 0 1.021.196 1.413.588.392.392.5877.86267.587 1.412v11c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-7V4.825l-1.6 1.6L8 5l4-4 4 4-1.4 1.425-1.6-1.6V16h-2Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 433 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 8.99985h3.5c.736 0 1.393.391 1.851 1.00095.32529-.60198.7255-1.16042 1.191-1.66195-.803-.823-1.866-1.339-3.042-1.339H4c-.26522 0-.51957.10536-.70711.29289C3.10536 7.48028 3 7.73463 3 7.99985s.10536.51957.29289.70711c.18754.18753.44189.29289.70711.29289Zm7.685 3.11095c.551-1.657 2.256-3.11095 3.649-3.11095h1.838l-1.293 1.29295c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3243.0928.0929.2031.1665.3244.2168.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2168L21 7.99985l-3.707-3.707c-.0928-.09285-.2031-.16649-.3244-.21674C16.8473 4.02586 16.7173 4 16.586 4c-.1313 0-.2613.02586-.3826.07611-.1213.05025-.2316.12389-.3244.21674-.0928.09284-.1665.20307-.2167.32437-.0503.12131-.0761.25133-.0761.38263 0 .1313.0258.26132.0761.38262.0502.12131.1239.23153.2167.32438l1.293 1.293h-1.838c-2.274 0-4.711 1.967-5.547 4.47895l-.472 1.411c-.641 1.926-2.072 3.11-2.815 3.11H4c-.26522 0-.51957.1054-.70711.2929-.18753.1876-.29289.4419-.29289.7071 0 .2653.10536.5196.29289.7072.18754.1875.44189.2928.70711.2928h2.5c1.837 0 3.863-1.925 4.713-4.479l.472-1.41Zm4.194 1.182c-.0929.0928-.1667.203-.217.3244-.0503.1213-.0762.2513-.0762.3826 0 .1314.0259.2614.0762.3827.0503.1214.1241.2316.217.3243l1.293 1.293h-2.338c-1.268 0-2.33-.891-2.691-2.108-.2661.773-.6326 1.5076-1.09 2.185.886 1.162 2.243 1.923 3.781 1.923h2.338l-1.293 1.293c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3244.0928.0928.2031.1664.3244.2167.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2167L21 16.9998l-3.707-3.707c-.0928-.0929-.203-.1666-.3243-.2169-.1213-.0504-.2514-.0763-.3827-.0763-.1313 0-.2614.0259-.3827.0763-.1213.0503-.2315.124-.3243.2169Z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 8.99985h3.5c.736 0 1.393.391 1.851 1.00095.32529-.60198.7255-1.16042 1.191-1.66195-.803-.823-1.866-1.339-3.042-1.339H4c-.26522 0-.51957.10536-.70711.29289C3.10536 7.48028 3 7.73463 3 7.99985s.10536.51957.29289.70711c.18754.18753.44189.29289.70711.29289Zm7.685 3.11095c.551-1.657 2.256-3.11095 3.649-3.11095h1.838l-1.293 1.29295c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3243.0928.0929.2031.1665.3244.2168.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2168L21 7.99985l-3.707-3.707c-.0928-.09285-.2031-.16649-.3244-.21674C16.8473 4.02586 16.7173 4 16.586 4c-.1313 0-.2613.02586-.3826.07611-.1213.05025-.2316.12389-.3244.21674-.0928.09284-.1665.20307-.2167.32437-.0503.12131-.0761.25133-.0761.38263 0 .1313.0258.26132.0761.38262.0502.12131.1239.23153.2167.32438l1.293 1.293h-1.838c-2.274 0-4.711 1.967-5.547 4.47895l-.472 1.411c-.641 1.926-2.072 3.11-2.815 3.11H4c-.26522 0-.51957.1054-.70711.2929-.18753.1876-.29289.4419-.29289.7071 0 .2653.10536.5196.29289.7072.18754.1875.44189.2928.70711.2928h2.5c1.837 0 3.863-1.925 4.713-4.479l.472-1.41Zm4.194 1.182c-.0929.0928-.1667.203-.217.3244-.0503.1213-.0762.2513-.0762.3826 0 .1314.0259.2614.0762.3827.0503.1214.1241.2316.217.3243l1.293 1.293h-2.338c-1.268 0-2.33-.891-2.691-2.108-.2661.773-.6326 1.5076-1.09 2.185.886 1.162 2.243 1.923 3.781 1.923h2.338l-1.293 1.293c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3244.0928.0928.2031.1664.3244.2167.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2167L21 16.9998l-3.707-3.707c-.0928-.0929-.203-.1666-.3243-.2169-.1213-.0504-.2514-.0763-.3827-.0763-.1313 0-.2614.0259-.3827.0763-.1213.0503-.2315.124-.3243.2169Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icons">
|
||||
<path id="Vector" d="M12.4105 1.91098C12.5668 1.75475 12.7787 1.66699 12.9997 1.66699C13.2206 1.66699 13.4326 1.75475 13.5888 1.91098L16.9222 5.24431C17.0784 5.40059 17.1662 5.61251 17.1662 5.83348C17.1662 6.05445 17.0784 6.26637 16.9222 6.42265L13.5888 9.75598C13.4317 9.90778 13.2212 9.99177 13.0027 9.98987C12.7842 9.98798 12.5752 9.90034 12.4207 9.74583C12.2662 9.59132 12.1785 9.38231 12.1766 9.16381C12.1747 8.94532 12.2587 8.73482 12.4105 8.57765L14.3213 6.66681H4.66634C4.44533 6.66681 4.23337 6.57902 4.07709 6.42274C3.92081 6.26646 3.83301 6.05449 3.83301 5.83348C3.83301 5.61247 3.92081 5.40051 4.07709 5.24423C4.23337 5.08794 4.44533 5.00015 4.66634 5.00015H14.3213L12.4105 3.08931C12.2543 2.93304 12.1665 2.72112 12.1665 2.50015C12.1665 2.27918 12.2543 2.06725 12.4105 1.91098ZM8.58884 10.2443C8.74507 10.4006 8.83283 10.6125 8.83283 10.8335C8.83283 11.0545 8.74507 11.2664 8.58884 11.4226L6.67801 13.3335H16.333C16.554 13.3335 16.766 13.4213 16.9223 13.5776C17.0785 13.7338 17.1663 13.9458 17.1663 14.1668C17.1663 14.3878 17.0785 14.5998 16.9223 14.7561C16.766 14.9123 16.554 15.0001 16.333 15.0001H6.67801L8.58884 16.911C8.74064 17.0681 8.82463 17.2786 8.82274 17.4971C8.82084 17.7156 8.7332 17.9247 8.57869 18.0792C8.42418 18.2337 8.21517 18.3213 7.99668 18.3232C7.77818 18.3251 7.56768 18.2411 7.41051 18.0893L4.07717 14.756C3.92095 14.5997 3.83319 14.3878 3.83319 14.1668C3.83319 13.9458 3.92095 13.7339 4.07717 13.5776L7.41051 10.2443C7.56678 10.0881 7.7787 10.0003 7.99967 10.0003C8.22064 10.0003 8.43257 10.0881 8.58884 10.2443Z" fill="#A3A3A3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 216 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5292 2.71983C10.3886 2.57938 10.1979 2.50049 9.99918 2.50049C9.80043 2.50049 9.6098 2.57938 9.46918 2.71983L5.21918 6.96983C5.0867 7.112 5.01457 7.30005 5.018 7.49435C5.02143 7.68865 5.10014 7.87404 5.23755 8.01145C5.37497 8.14886 5.56035 8.22758 5.75465 8.231C5.94896 8.23443 6.137 8.16231 6.27918 8.02983L9.99918 4.30983L13.7192 8.02983C13.7878 8.10351 13.8706 8.16262 13.9626 8.20361C14.0546 8.2446 14.154 8.26664 14.2547 8.26842C14.3554 8.2702 14.4554 8.25167 14.5488 8.21395C14.6422 8.17623 14.727 8.12009 14.7982 8.04887C14.8694 7.97765 14.9256 7.89281 14.9633 7.79943C15.001 7.70604 15.0195 7.60601 15.0178 7.50531C15.016 7.4046 14.994 7.30529 14.953 7.21329C14.912 7.12129 14.8529 7.03849 14.7792 6.96983L10.5292 2.71983ZM14.7792 13.0298L10.5292 17.2798C10.3886 17.4203 10.1979 17.4992 9.99918 17.4992C9.80043 17.4992 9.6098 17.4203 9.46918 17.2798L5.21918 13.0298C5.14549 12.9612 5.08639 12.8784 5.0454 12.7864C5.0044 12.6944 4.98236 12.5951 4.98059 12.4944C4.97881 12.3936 4.99733 12.2936 5.03505 12.2002C5.07278 12.1068 5.12892 12.022 5.20014 11.9508C5.27136 11.8796 5.35619 11.8234 5.44958 11.7857C5.54297 11.748 5.643 11.7295 5.7437 11.7312C5.8444 11.733 5.94372 11.7551 6.03571 11.796C6.12771 11.837 6.21051 11.8961 6.27918 11.9698L9.99918 15.6898L13.7192 11.9698C13.7878 11.8961 13.8706 11.837 13.9626 11.796C14.0546 11.7551 14.154 11.733 14.2547 11.7312C14.3554 11.7295 14.4554 11.748 14.5488 11.7857C14.6422 11.8234 14.727 11.8796 14.7982 11.9508C14.8694 12.022 14.9256 12.1068 14.9633 12.2002C15.001 12.2936 15.0195 12.3936 15.0178 12.4944C15.016 12.5951 14.994 12.6944 14.953 12.7864C14.912 12.8784 14.8529 12.9612 14.7792 13.0298Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6ZM11 16V7.85L8.4 10.45L7 9L12 4L17 9L15.6 10.45L13 7.85V16H11Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 357 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 20c-.55 0-1.021-.196-1.413-.588C4.195 19.02 3.99934 18.5493 4 18v-3h2v3h12v-3h2v3c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-4V7.85l-2.6 2.6L7 9l5-5 5 5-1.4 1.45-2.6-2.6V16h-2Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 308 B |
@@ -1,10 +0,0 @@
|
||||
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<path d="M15.945 21.15a10.498 10.498 0 1 1 19.11 8.7A10.47 10.47 0 0 1 25.5 36c-4.05 0-7.755-2.34-9.495-6H1.5v-3c.09-1.71 1.26-3.105 3.51-4.23 2.25-1.125 5.07-1.71 8.49-1.77.855 0 1.665.075 2.445.15ZM13.5 6c1.68.045 3.09.63 4.215 1.755s1.68 2.535 1.68 4.245-.555 3.12-1.68 4.245-2.535 1.68-4.215 1.68c-1.68 0-3.09-.555-4.215-1.68S7.605 13.71 7.605 12s.555-3.12 1.68-4.245S11.82 6.045 13.5 6Zm12 27a7.5 7.5 0 1 0 0-15 7.5 7.5 0 0 0 0 15ZM24 21h2.25v4.23l3.66 2.115-1.125 1.95L24 26.535V21Z" fill="#fff"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h36v36H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 725 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6a6 6 0 1 1 0 12 6 6 0 0 1 0-12Zm0 15c6.63 0 12 2.685 12 6v3H6v-3c0-3.315 5.37-6 12-6Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 200 B |
@@ -1,18 +0,0 @@
|
||||
export function Back() {
|
||||
return (
|
||||
<svg
|
||||
width="36"
|
||||
height="36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.546 8 8 17.546l9.546 9.546"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export function Paste() {
|
||||
return (
|
||||
<svg
|
||||
width="36"
|
||||
height="36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="currentColor" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export function Scan() {
|
||||
return (
|
||||
<svg
|
||||
width="37"
|
||||
height="36"
|
||||
viewBox="0 0 37 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,22 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
import { A } from "@solidjs/router";
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch
|
||||
} from "solid-js";
|
||||
import { cache, createAsync, revalidate, useNavigate } from "@solidjs/router";
|
||||
import { Plus, Save, Search, Shuffle, Users } from "lucide-solid";
|
||||
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
|
||||
import {
|
||||
ActivityDetailsModal,
|
||||
ActivityItem,
|
||||
HackActivityType,
|
||||
LoadingShimmer,
|
||||
NiceP
|
||||
} from "~/components";
|
||||
import { ActivityDetailsModal, ButtonCard, NiceP } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { createDeepSignal } from "~/utils";
|
||||
import { timeAgo } from "~/utils";
|
||||
|
||||
interface IActivityItem {
|
||||
import { GenericItem } from "./GenericItem";
|
||||
|
||||
export type HackActivityType =
|
||||
| "Lightning"
|
||||
| "OnChain"
|
||||
| "ChannelOpen"
|
||||
| "ChannelClose";
|
||||
|
||||
export interface IActivityItem {
|
||||
kind: HackActivityType;
|
||||
id: string;
|
||||
amount_sats: number;
|
||||
@@ -31,38 +26,139 @@ interface IActivityItem {
|
||||
last_updated: number;
|
||||
}
|
||||
|
||||
function UnifiedActivityItem(props: {
|
||||
export function UnifiedActivityItem(props: {
|
||||
item: IActivityItem;
|
||||
onClick: (id: string, kind: HackActivityType) => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const click = () => {
|
||||
props.onClick(
|
||||
props.item.id,
|
||||
props.item.kind as unknown as HackActivityType
|
||||
);
|
||||
};
|
||||
|
||||
const primaryContact = () => {
|
||||
if (props.item.contacts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return props.item.contacts[0];
|
||||
};
|
||||
|
||||
// TODO: figure out what other shit we should filter out
|
||||
const message = () => {
|
||||
const filtered = props.item.labels.filter(
|
||||
(l) => l !== "SWAP" && !l.startsWith("LN Channel:")
|
||||
);
|
||||
if (filtered.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return filtered[0];
|
||||
};
|
||||
|
||||
const shouldShowShuffle = () => {
|
||||
return (
|
||||
props.item.kind === "ChannelOpen" ||
|
||||
props.item.kind === "ChannelClose" ||
|
||||
(props.item.labels.length > 0 && props.item.labels[0] === "SWAP")
|
||||
);
|
||||
};
|
||||
|
||||
const verb = () => {
|
||||
if (props.item.kind === "ChannelOpen") {
|
||||
return "opened a";
|
||||
}
|
||||
if (props.item.kind === "ChannelClose") {
|
||||
return "closed a";
|
||||
}
|
||||
if (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") {
|
||||
return "swapped to";
|
||||
}
|
||||
if (
|
||||
props.item.labels.length > 0 &&
|
||||
props.item.labels[0] === "Swept Force Close"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return "sent";
|
||||
};
|
||||
|
||||
const primaryName = () => {
|
||||
return props.item.inbound ? primaryContact()?.name || "Unknown" : "You";
|
||||
};
|
||||
|
||||
const secondaryName = () => {
|
||||
if (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") {
|
||||
return "Lightning";
|
||||
}
|
||||
if (
|
||||
props.item.kind === "ChannelOpen" ||
|
||||
props.item.kind === "ChannelClose"
|
||||
) {
|
||||
return "Lightning channel";
|
||||
}
|
||||
if (!props.item.inbound) {
|
||||
return primaryContact()?.name || "Unknown";
|
||||
}
|
||||
return "you";
|
||||
};
|
||||
|
||||
const shouldShowGeneric = () => {
|
||||
if (props.item.inbound && primaryName() === "Unknown") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!props.item.inbound && secondaryName() === "Unknown") {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActivityItem
|
||||
// This is actually the ActivityType enum but wasm is hard
|
||||
kind={props.item.kind as unknown as HackActivityType}
|
||||
labels={props.item.labels}
|
||||
contacts={props.item.contacts}
|
||||
// FIXME: is this something we can put into node logic?
|
||||
amount={props.item.amount_sats || 0}
|
||||
date={props.item.last_updated}
|
||||
positive={props.item.inbound}
|
||||
onClick={click}
|
||||
/>
|
||||
<div class="pt-3 first-of-type:pt-0">
|
||||
<GenericItem
|
||||
primaryAvatarUrl={primaryContact()?.image_url || ""}
|
||||
icon={shouldShowShuffle() ? <Shuffle /> : undefined}
|
||||
primaryOnClick={() =>
|
||||
primaryName() !== "You" && primaryContact()?.id
|
||||
? navigate(`/chat/${primaryContact()?.id}`)
|
||||
: undefined
|
||||
}
|
||||
amountOnClick={click}
|
||||
primaryName={
|
||||
props.item.inbound
|
||||
? primaryContact()?.name || "Unknown"
|
||||
: "You"
|
||||
}
|
||||
genericAvatar={shouldShowGeneric()}
|
||||
verb={verb()}
|
||||
message={message()}
|
||||
secondaryName={secondaryName()}
|
||||
amount={
|
||||
props.item.amount_sats
|
||||
? BigInt(props.item.amount_sats || 0)
|
||||
: undefined
|
||||
}
|
||||
date={timeAgo(props.item.last_updated)}
|
||||
accent={props.item.inbound ? "green" : undefined}
|
||||
visibility={
|
||||
props.item.kind === "Lightning" ? "private" : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CombinedActivity(props: { limit?: number }) {
|
||||
export function CombinedActivity() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||
const [detailsId, setDetailsId] = createSignal("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
function openDetailsModal(id: string, kind: HackActivityType) {
|
||||
console.log("Opening details modal: ", id, kind);
|
||||
@@ -77,26 +173,28 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
setDetailsOpen(true);
|
||||
}
|
||||
|
||||
async function fetchActivity() {
|
||||
return await state.mutiny_wallet?.get_activity();
|
||||
}
|
||||
const getActivity = cache(async () => {
|
||||
try {
|
||||
console.log("refetching activity");
|
||||
const activity = await state.mutiny_wallet?.get_activity();
|
||||
return (activity || []) as IActivityItem[];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [] as IActivityItem[];
|
||||
}
|
||||
}, "activity");
|
||||
|
||||
const [activity, { refetch }] = createResource(fetchActivity, {
|
||||
storage: createDeepSignal
|
||||
});
|
||||
const activity = createAsync(() => getActivity(), { initialValue: [] });
|
||||
|
||||
createEffect(() => {
|
||||
// Should re-run after every sync
|
||||
if (!state.is_syncing) {
|
||||
refetch();
|
||||
revalidate("activity");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={activity.state === "ready" || activity.state === "refreshing"}
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<>
|
||||
<Show when={detailsId() && detailsKind()}>
|
||||
<ActivityDetailsModal
|
||||
open={detailsOpen()}
|
||||
@@ -106,47 +204,63 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
/>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={activity.latest.length === 0}>
|
||||
<div class="w-full pb-4 text-center">
|
||||
<NiceP>
|
||||
{i18n.t(
|
||||
"activity.receive_some_sats_to_get_started"
|
||||
<Match when={activity().length === 0}>
|
||||
<Show when={state.federations?.length === 0}>
|
||||
<ButtonCard
|
||||
onClick={() => navigate("/settings/federations")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Users class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.federation")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Show>
|
||||
<ButtonCard onClick={() => navigate("/receive")}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Plus class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.receive")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
<ButtonCard onClick={() => navigate("/search")}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.find")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
<Show when={!state.has_backed_up}>
|
||||
<ButtonCard
|
||||
onClick={() => navigate("/settings/backup")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Save class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.backup")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={activity().length >= 0}>
|
||||
<Show when={!state.has_backed_up}>
|
||||
<ButtonCard
|
||||
onClick={() => navigate("/settings/backup")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Save class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.backup")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Show>
|
||||
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
|
||||
<For each={activity()}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</NiceP>
|
||||
</For>
|
||||
</div>
|
||||
</Match>
|
||||
<Match
|
||||
when={props.limit && activity.latest.length > props.limit}
|
||||
>
|
||||
<For each={activity.latest.slice(0, props.limit)}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={activity.latest.length >= 0}>
|
||||
<For each={activity.latest}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
{/* Only show on the home screen */}
|
||||
<Show when={props.limit}>
|
||||
<A
|
||||
href="/activity"
|
||||
class="self-center font-semibold text-m-red no-underline active:text-m-red/80"
|
||||
>
|
||||
{i18n.t("activity.view_all")}
|
||||
</A>
|
||||
</Show>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,22 +4,19 @@ import {
|
||||
MutinyInvoice,
|
||||
TagItem
|
||||
} from "@mutinywallet/mutiny-wasm";
|
||||
import { Copy, Link, Shuffle, Zap } from "lucide-solid";
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
Match,
|
||||
ParentComponent,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch
|
||||
} from "solid-js";
|
||||
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import chain from "~/assets/icons/chain.svg";
|
||||
import copyIcon from "~/assets/icons/copy.svg";
|
||||
import shuffle from "~/assets/icons/shuffle.svg";
|
||||
import {
|
||||
ActivityAmount,
|
||||
AmountFiat,
|
||||
AmountSats,
|
||||
FancyCard,
|
||||
@@ -59,6 +56,39 @@ interface OnChainTx {
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
const ActivityAmount: ParentComponent<{
|
||||
amount: string;
|
||||
price: number;
|
||||
positive?: boolean;
|
||||
center?: boolean;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div
|
||||
class="flex flex-col gap-1"
|
||||
classList={{
|
||||
"items-end": !props.center,
|
||||
"items-center": props.center
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="justify-end"
|
||||
classList={{ "text-m-green": props.positive }}
|
||||
>
|
||||
<AmountSats
|
||||
amountSats={Number(props.amount)}
|
||||
icon={props.positive ? "plus" : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={Number(props.amount)}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
export const DIALOG_POSITIONER =
|
||||
"fixed inset-0 z-50 flex items-center justify-center";
|
||||
@@ -74,7 +104,7 @@ function LightningHeader(props: { info: MutinyInvoice }) {
|
||||
{props.info.inbound
|
||||
? i18n.t("activity.transaction_details.lightning_receive")
|
||||
: i18n.t("activity.transaction_details.lightning_send")}
|
||||
<img src={bolt} alt="lightning bolt" class="h-4 w-4" />
|
||||
<Zap class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
@@ -130,10 +160,10 @@ function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
props.kind === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<img src={shuffle} alt="swap" class="h-4 w-4" />
|
||||
<Shuffle class="h-4 w-4" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={chain} alt="blockchain" class="h-4 w-4" />
|
||||
<Link class="h-4 w-4" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
@@ -172,7 +202,7 @@ export function MiniStringShower(props: { text: string }) {
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.text)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="h-4 w-4" />
|
||||
<Copy class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -215,11 +245,13 @@ function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {
|
||||
</TinyButton>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.status")}>
|
||||
{props.info.paid
|
||||
? i18n.t("activity.transaction_details.paid")
|
||||
: i18n.t("activity.transaction_details.unpaid")}
|
||||
</KeyValue>
|
||||
<Show when={!props.info.paid}>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.status")}
|
||||
>
|
||||
i18n.t("activity.transaction_details.unpaid")
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.date")}>
|
||||
<FormatPrettyPrint ts={Number(props.info.last_updated)} />
|
||||
</KeyValue>
|
||||
@@ -233,12 +265,7 @@ function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {
|
||||
<KeyValue key={i18n.t("activity.transaction_details.invoice")}>
|
||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.payment_hash")}
|
||||
>
|
||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||
</KeyValue>
|
||||
<Show when={props.info.paid}>
|
||||
<Show when={props.info.paid && !props.info.inbound}>
|
||||
<KeyValue
|
||||
key={i18n.t(
|
||||
"activity.transaction_details.payment_preimage"
|
||||
@@ -369,11 +396,6 @@ function OnchainDetails(props: {
|
||||
"Pending"
|
||||
)}
|
||||
</KeyValue>
|
||||
<Show when={props.kind === "ChannelOpen" && channelInfo()}>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.peer")}>
|
||||
<MiniStringShower text={channelInfo()?.peer ?? ""} />
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.txid")}>
|
||||
<div class="flex gap-1">
|
||||
{/* Have to do all these shenanigans because css / html is hard */}
|
||||
@@ -412,7 +434,7 @@ function OnchainDetails(props: {
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.info.txid)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="h-4 w-4" />
|
||||
<Copy class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</KeyValue>
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
import { Match, ParentComponent, Switch } from "solid-js";
|
||||
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import chain from "~/assets/icons/chain.svg";
|
||||
import shuffle from "~/assets/icons/shuffle.svg";
|
||||
import { AmountFiat, AmountSats, LabelCircle } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { timeAgo } from "~/utils";
|
||||
|
||||
export const ActivityAmount: ParentComponent<{
|
||||
amount: string;
|
||||
price: number;
|
||||
positive?: boolean;
|
||||
center?: boolean;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div
|
||||
class="flex flex-col gap-1"
|
||||
classList={{
|
||||
"items-end": !props.center,
|
||||
"items-center": props.center
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="justify-end"
|
||||
classList={{ "text-m-green": props.positive }}
|
||||
>
|
||||
<AmountSats
|
||||
amountSats={Number(props.amount)}
|
||||
icon={props.positive ? "plus" : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={Number(props.amount)}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type HackActivityType =
|
||||
| "Lightning"
|
||||
| "OnChain"
|
||||
| "ChannelOpen"
|
||||
| "ChannelClose";
|
||||
|
||||
export function ActivityItem(props: {
|
||||
// This is actually the ActivityType enum but wasm is hard
|
||||
kind: HackActivityType;
|
||||
contacts: TagItem[];
|
||||
labels: string[];
|
||||
amount: number | bigint;
|
||||
date?: number | bigint;
|
||||
positive?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const firstContact = () =>
|
||||
props.contacts?.length ? props.contacts[0] : null;
|
||||
|
||||
// TODO: pass a value to the timeago function that will cause it to recalculate on sync
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.onClick && props.onClick()}
|
||||
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] gap-4 border-b border-neutral-800 pb-4 last:border-b-0"
|
||||
classList={{ "cursor-pointer": !!props.onClick }}
|
||||
>
|
||||
<div class="flex items-center gap-2 md:gap-4">
|
||||
<div class="">
|
||||
<Switch>
|
||||
<Match when={props.kind === "Lightning"}>
|
||||
<img class="w-[1rem]" src={bolt} alt="lightning" />
|
||||
</Match>
|
||||
<Match when={props.kind === "OnChain"}>
|
||||
<img class="w-[1rem]" src={chain} alt="onchain" />
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
props.kind === "ChannelOpen" ||
|
||||
props.kind === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<img class="w-[1rem]" src={shuffle} alt="swap" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="">
|
||||
<LabelCircle
|
||||
name={firstContact()?.name}
|
||||
image_url={firstContact()?.image_url}
|
||||
contact={props.contacts?.length > 0}
|
||||
label={props.labels?.length > 0}
|
||||
channel={props.kind}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Switch>
|
||||
<Match when={props.kind === "ChannelClose"}>
|
||||
<span class="text-base font-semibold text-neutral-500">
|
||||
{i18n.t("activity.channel_close")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={props.kind === "ChannelOpen"}>
|
||||
<span class="text-base font-semibold text-neutral-500">
|
||||
{i18n.t("activity.channel_open")}
|
||||
</span>{" "}
|
||||
</Match>
|
||||
<Match when={firstContact()?.name}>
|
||||
<span class="truncate text-base font-semibold">
|
||||
{firstContact()?.name}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={props.labels.length > 0}>
|
||||
<span class="truncate text-base font-semibold">
|
||||
{props.labels[0]}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={props.positive}>
|
||||
<span class="text-base font-semibold text-neutral-500">
|
||||
{i18n.t("activity.unknown")}
|
||||
</span>
|
||||
</Match>
|
||||
|
||||
<Match when={!props.positive}>
|
||||
<span class="text-base font-semibold text-neutral-500">
|
||||
{i18n.t("activity.unknown")}
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Switch>
|
||||
<Match when={props.date && props.date > 2147483647}>
|
||||
<time class="text-sm text-m-yellow">
|
||||
{i18n.t("common.pending")}
|
||||
</time>
|
||||
</Match>
|
||||
<Match when={timeAgo(props.date) === "Pending"}>
|
||||
<time class="text-sm text-m-yellow">
|
||||
{i18n.t("common.pending")}
|
||||
</time>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<time class="text-sm text-neutral-500">
|
||||
{timeAgo(props.date)}
|
||||
</time>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="">
|
||||
<Switch>
|
||||
<Match when={props.kind === "ChannelClose"}>
|
||||
<div />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<ActivityAmount
|
||||
amount={props.amount.toString()}
|
||||
price={state.price}
|
||||
positive={props.positive}
|
||||
/>
|
||||
</Match>{" "}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Link, Users, Zap } from "lucide-solid";
|
||||
import { Show } from "solid-js";
|
||||
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import chain from "~/assets/icons/chain.svg";
|
||||
import community from "~/assets/icons/community.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToFormattedFiat } from "~/utils";
|
||||
@@ -24,13 +22,13 @@ export function AmountSats(props: {
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={props.icon === "lightning"}>
|
||||
<img src={bolt} alt="lightning" class="h-[18px]" />
|
||||
<Zap class="w-[18px]" />
|
||||
</Show>
|
||||
<Show when={props.icon === "community"}>
|
||||
<img src={community} alt="community" class="h-[18px]" />
|
||||
<Users class="w-[18px]" />
|
||||
</Show>
|
||||
<Show when={props.icon === "chain"}>
|
||||
<img src={chain} alt="chain" class="h-[18px]" />
|
||||
<Link class="w-[18px]" />
|
||||
</Show>
|
||||
<h1 class="whitespace-nowrap text-right font-light">
|
||||
<Show when={props.icon === "plus"}>
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Shuffle } from "lucide-solid";
|
||||
import { Match, Show, Switch } from "solid-js";
|
||||
|
||||
import shuffle from "~/assets/icons/shuffle.svg";
|
||||
import {
|
||||
AmountFiat,
|
||||
AmountSats,
|
||||
Button,
|
||||
FancyCard,
|
||||
Indicator,
|
||||
InfoBox
|
||||
InfoBox,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function LoadingShimmer(props: { center?: boolean }) {
|
||||
export function LoadingShimmer(props: { center?: boolean; small?: boolean }) {
|
||||
return (
|
||||
<div class="flex animate-pulse flex-col gap-2">
|
||||
<h1
|
||||
class="text-4xl font-light"
|
||||
classList={{ "flex justify-center": props.center }}
|
||||
>
|
||||
<div class="h-[2.5rem] w-[12rem] rounded bg-neutral-700" />
|
||||
<div
|
||||
class="rounded bg-neutral-700"
|
||||
classList={{
|
||||
"h-[2.5rem] w-[12rem]": !props.small,
|
||||
"h-[1rem] w-[8rem]": props.small
|
||||
}}
|
||||
/>
|
||||
</h1>
|
||||
<h2
|
||||
class="text-xl font-light text-white/70"
|
||||
classList={{ "flex justify-center": props.center }}
|
||||
>
|
||||
<div class="h-[1.75rem] w-[8rem] rounded bg-neutral-700" />
|
||||
</h2>
|
||||
<Show when={!props.small}>
|
||||
<h2
|
||||
class="text-xl font-light text-white/70"
|
||||
classList={{ "flex justify-center": props.center }}
|
||||
>
|
||||
<div class="h-[1.75rem] w-[8rem] rounded bg-neutral-700" />
|
||||
</h2>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,19 +43,10 @@ export function LoadingShimmer(props: { center?: boolean }) {
|
||||
const STYLE =
|
||||
"px-2 py-1 rounded-xl text-sm flex gap-2 items-center font-semibold";
|
||||
|
||||
export function BalanceBox(props: { loading?: boolean }) {
|
||||
export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const emptyBalance = () =>
|
||||
(state.balance?.confirmed || 0n) === 0n &&
|
||||
(state.balance?.lightning || 0n) === 0n &&
|
||||
(state.balance?.federation || 0n) === 0n &&
|
||||
(state.balance?.force_close || 0n) === 0n &&
|
||||
(state.balance?.unconfirmed || 0n) === 0n;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const totalOnchain = () =>
|
||||
(state.balance?.confirmed || 0n) +
|
||||
(state.balance?.unconfirmed || 0n) +
|
||||
@@ -57,8 +56,8 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
(state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FancyCard>
|
||||
<VStack>
|
||||
<FancyCard title="Lightning">
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<Switch>
|
||||
<Match when={state.safe_mode}>
|
||||
@@ -91,15 +90,16 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
<Show when={state.federations && state.federations.length}>
|
||||
</FancyCard>
|
||||
<Show when={state.federations && state.federations.length}>
|
||||
<FancyCard title="Fedimint">
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<hr class="my-2 border-m-grey-750" />
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-2xl">
|
||||
<AmountSats
|
||||
amountSats={
|
||||
state.balance?.federation || 0
|
||||
state.balance?.federation || 0n
|
||||
}
|
||||
icon="community"
|
||||
denominationSize="lg"
|
||||
@@ -115,21 +115,20 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={state.balance?.federation || 0n > 0n}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swaplightning" class={STYLE}>
|
||||
<img
|
||||
src={shuffle}
|
||||
alt="swaplightning"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
<Shuffle class="h-6 w-6" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<hr class="my-2 border-m-grey-750" />
|
||||
</FancyCard>
|
||||
</Show>
|
||||
<FancyCard title="On-chain">
|
||||
{/* <hr class="my-2 border-m-grey-750" /> */}
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -159,11 +158,7 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
<Show when={usableOnchain() > 0n}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swap" class={STYLE}>
|
||||
<img
|
||||
src={shuffle}
|
||||
alt="swap"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
<Shuffle class="h-6 w-6" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -171,22 +166,6 @@ export function BalanceBox(props: { loading?: boolean }) {
|
||||
</div>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
<div class="flex gap-2 py-4">
|
||||
<Button
|
||||
onClick={() => navigate("/search")}
|
||||
disabled={emptyBalance() || props.loading}
|
||||
intent="green"
|
||||
>
|
||||
{i18n.t("common.send")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate("/receive")}
|
||||
disabled={props.loading}
|
||||
intent="blue"
|
||||
>
|
||||
{i18n.t("common.receive")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ArrowDownUp } from "lucide-solid";
|
||||
import { Show } from "solid-js";
|
||||
|
||||
import currencySwap from "~/assets/icons/currency-swap.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Currency } from "~/utils";
|
||||
@@ -51,7 +51,7 @@ function SmallSubtleAmount(props: {
|
||||
|
||||
return (
|
||||
<h2
|
||||
class="flex flex-row items-end whitespace-nowrap text-xl font-light text-neutral-400"
|
||||
class="flex flex-row items-center whitespace-nowrap text-xl font-light text-neutral-400"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Show when={!props.loading || props.mode === "fiat"} fallback="…">
|
||||
@@ -65,13 +65,7 @@ function SmallSubtleAmount(props: {
|
||||
<span class="text-base">
|
||||
{props.fiat ? props.fiat.value : i18n.t("common.sats")}
|
||||
</span>
|
||||
<img
|
||||
class={"pb-[4px] pl-[4px] hover:cursor-pointer"}
|
||||
src={currencySwap}
|
||||
height={24}
|
||||
width={24}
|
||||
alt="Swap currencies"
|
||||
/>
|
||||
<ArrowDownUp class="flex-0 inline-block h-6 w-6 pl-2 hover:cursor-pointer" />
|
||||
</Show>
|
||||
</h2>
|
||||
);
|
||||
|
||||
28
src/components/ContactButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
|
||||
import { LabelCircle } from "~/components";
|
||||
import { PseudoContact } from "~/utils";
|
||||
|
||||
export function ContactButton(props: {
|
||||
contact: PseudoContact | TagItem;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button class="flex items-center gap-2" onClick={() => props.onClick()}>
|
||||
<LabelCircle
|
||||
name={props.contact.name}
|
||||
image_url={props.contact.primal_image_url}
|
||||
contact
|
||||
label={false}
|
||||
/>
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<h2 class="overflow-hidden overflow-ellipsis text-base font-semibold">
|
||||
{props.contact.name}
|
||||
</h2>
|
||||
<h3 class="overflow-hidden overflow-ellipsis text-left text-xs font-normal text-m-grey-400">
|
||||
{props.contact.ln_address || ""}
|
||||
</h3>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SubmitHandler } from "@modular-forms/solid";
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { createSignal, Match, Show, Switch } from "solid-js";
|
||||
import { createSignal, JSX, Match, Show, Switch } from "solid-js";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
MiniStringShower,
|
||||
showToast,
|
||||
SimpleDialog,
|
||||
SmallHeader,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
@@ -25,8 +24,8 @@ export type ContactFormValues = {
|
||||
};
|
||||
|
||||
export function ContactViewer(props: {
|
||||
children: JSX.Element;
|
||||
contact: TagItem;
|
||||
gradient: string;
|
||||
saveContact: (id: string, contact: ContactFormValues) => void;
|
||||
deleteContact: (id: string) => Promise<void>;
|
||||
}) {
|
||||
@@ -80,25 +79,7 @@ export function ContactViewer(props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
class="flex w-16 flex-shrink-0 flex-col items-center gap-2 overflow-x-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex h-16 w-16 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-4xl uppercase"
|
||||
style={{ background: props.gradient }}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={props.contact.image_url}>
|
||||
<img src={props.contact.image_url} />
|
||||
</Match>
|
||||
<Match when={true}>{props.contact.name[0]}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<SmallHeader class="h-4 w-16 overflow-hidden overflow-ellipsis text-center">
|
||||
{props.contact.name}
|
||||
</SmallHeader>
|
||||
</button>
|
||||
<button onClick={() => setIsOpen(true)}>{props.children}</button>
|
||||
<SimpleDialog
|
||||
open={isOpen()}
|
||||
setOpen={setIsOpen}
|
||||
@@ -129,12 +110,7 @@ export function ContactViewer(props: {
|
||||
<Match when={!isEditing()}>
|
||||
<div class="mx-auto flex w-full max-w-[400px] flex-1 flex-col items-center justify-around gap-4">
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<div
|
||||
class="flex h-32 w-32 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-8xl uppercase"
|
||||
style={{
|
||||
background: props.gradient
|
||||
}}
|
||||
>
|
||||
<div class="flex h-32 w-32 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-8xl uppercase">
|
||||
<Switch>
|
||||
<Match when={props.contact.image_url}>
|
||||
<img
|
||||
|
||||
@@ -20,6 +20,9 @@ export function DeleteEverything(props: { emergency?: boolean }) {
|
||||
async function resetNode() {
|
||||
try {
|
||||
setConfirmLoading(true);
|
||||
|
||||
localStorage.removeItem("profile_setup_stage");
|
||||
|
||||
// If we're in a context where the wallet is loaded we want to use the regular action to delete it
|
||||
// Otherwise we just call the import_json method directly
|
||||
if (state.mutiny_wallet && !props.emergency) {
|
||||
|
||||
88
src/components/EditProfileForm.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createFileUploader } from "@solid-primitives/upload";
|
||||
import { createSignal, Match, Switch } from "solid-js";
|
||||
|
||||
import { Button, SimpleInput } from "~/components";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { blobToBase64 } from "~/utils";
|
||||
|
||||
export type EditableProfile = {
|
||||
nym?: string;
|
||||
imageUrl?: string;
|
||||
};
|
||||
|
||||
export function EditProfileForm(props: {
|
||||
initialProfile?: EditableProfile;
|
||||
onSave: (profile: EditableProfile) => Promise<void>;
|
||||
saving: boolean;
|
||||
cta: string;
|
||||
}) {
|
||||
const [state] = useMegaStore();
|
||||
const [nym, setNym] = createSignal(props.initialProfile?.nym || "");
|
||||
const [uploading, setUploading] = createSignal(false);
|
||||
|
||||
const { files, selectFiles } = createFileUploader({
|
||||
multiple: false,
|
||||
accept: "image/*"
|
||||
});
|
||||
|
||||
async function uploadFile() {
|
||||
selectFiles(async (files) => {
|
||||
if (files.length) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
let imageUrl;
|
||||
if (files() && files().length) {
|
||||
setUploading(true);
|
||||
const base64 = await blobToBase64(files()[0].file);
|
||||
if (base64) {
|
||||
imageUrl =
|
||||
await state.mutiny_wallet?.upload_profile_pic(base64);
|
||||
}
|
||||
setUploading(false);
|
||||
}
|
||||
await props.onSave({
|
||||
nym: nym(),
|
||||
imageUrl: imageUrl ? imageUrl : props.initialProfile?.imageUrl
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
class="shiny-button flex h-[8rem] w-[8rem] flex-none items-center justify-center self-center overflow-clip rounded-full bg-m-grey-800 text-5xl uppercase"
|
||||
onClick={uploadFile}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={files() && files().length}>
|
||||
<img src={files()[0].source} />
|
||||
</Match>
|
||||
<Match when={props.initialProfile?.imageUrl}>
|
||||
<img src={props.initialProfile?.imageUrl} />
|
||||
</Match>
|
||||
<Match when={true}>+</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
<SimpleInput
|
||||
value={nym()}
|
||||
onInput={(e) => setNym(e.currentTarget.value)}
|
||||
placeholder="Your name or nym"
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<Button
|
||||
layout="full"
|
||||
onClick={onSave}
|
||||
loading={props.saving || uploading()}
|
||||
>
|
||||
{props.cta}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
NiceP,
|
||||
SafeArea,
|
||||
SmallHeader
|
||||
} from "~/components/layout";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
@@ -28,37 +27,32 @@ export function ErrorDisplay(props: { error: Error }) {
|
||||
console.error(props.error);
|
||||
});
|
||||
return (
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<Title>{i18n.t("error.general.oh_no")}</Title>
|
||||
<DefaultMain>
|
||||
<LargeHeader>{i18n.t("error.title")}</LargeHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t("error.general.never_should_happen")}
|
||||
</SmallHeader>
|
||||
<SimpleErrorDisplay error={props.error} />
|
||||
<NiceP>
|
||||
{i18n.t("error.general.try_reloading")}{" "}
|
||||
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||
{i18n.t("error.general.support_link")}
|
||||
</ExternalLink>
|
||||
</NiceP>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
{i18n.t("error.reload")}
|
||||
</Button>
|
||||
<NiceP>
|
||||
{i18n.t("error.general.getting_desperate")}{" "}
|
||||
<A href="/settings/emergencykit">
|
||||
{i18n.t("error.emergency_link")}
|
||||
</A>
|
||||
</NiceP>
|
||||
<div class="h-full" />
|
||||
<Button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
intent="red"
|
||||
>
|
||||
{i18n.t("common.dangit")}
|
||||
</Button>
|
||||
</DefaultMain>
|
||||
</SafeArea>
|
||||
<LargeHeader>{i18n.t("error.title")}</LargeHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t("error.general.never_should_happen")}
|
||||
</SmallHeader>
|
||||
<SimpleErrorDisplay error={props.error} />
|
||||
<NiceP>
|
||||
{i18n.t("error.general.try_reloading")}{" "}
|
||||
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||
{i18n.t("error.general.support_link")}
|
||||
</ExternalLink>
|
||||
</NiceP>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
{i18n.t("error.reload")}
|
||||
</Button>
|
||||
<NiceP>
|
||||
{i18n.t("error.general.getting_desperate")}{" "}
|
||||
<A href="/settings/emergencykit">
|
||||
{i18n.t("error.emergency_link")}
|
||||
</A>
|
||||
</NiceP>
|
||||
<div class="h-full" />
|
||||
<Button onClick={() => (window.location.href = "/")} intent="red">
|
||||
{i18n.t("common.dangit")}
|
||||
</Button>
|
||||
</DefaultMain>
|
||||
);
|
||||
}
|
||||
|
||||
171
src/components/Fab.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { ArrowDownLeft, ArrowUpRight, Plus, Scan } from "lucide-solid";
|
||||
import { createSignal, JSX, onCleanup, onMount, Show } from "solid-js";
|
||||
|
||||
import { Circle } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
function FabMenuItem(props: {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
children: JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
class="flex gap-2 px-2 py-4 disabled:opacity-50"
|
||||
disabled={props.disabled}
|
||||
onClick={() => props.onClick()}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function FabMenu(props: {
|
||||
setOpen: (open: boolean) => void;
|
||||
children: JSX.Element;
|
||||
right?: boolean;
|
||||
left?: boolean;
|
||||
}) {
|
||||
let navRef: HTMLElement;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (e.target instanceof Element && !navRef.contains(e.target)) {
|
||||
e.stopPropagation();
|
||||
props.setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.body.addEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
document.body.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={(el) => (navRef = el)}
|
||||
class="fixed z-50 rounded-xl bg-m-grey-800/90 px-2 backdrop-blur-lg"
|
||||
classList={{
|
||||
"right-8 bottom-[calc(2rem+5rem)]": props.right,
|
||||
"left-2 bottom-[calc(2rem+2rem)]": props.left
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function Fab(props: { onSearch: () => void; onScan: () => void }) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const navigate = useNavigate();
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={open()}>
|
||||
<FabMenu setOpen={setOpen} right>
|
||||
<ul class="flex flex-col divide-y divide-m-grey-400/25">
|
||||
<li>
|
||||
<FabMenuItem
|
||||
onClick={() => {
|
||||
props.onSearch();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<ArrowUpRight />
|
||||
{i18n.t("common.send")}
|
||||
</FabMenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<FabMenuItem onClick={() => navigate("/receive")}>
|
||||
<ArrowDownLeft />
|
||||
{i18n.t("common.receive")}
|
||||
</FabMenuItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<FabMenuItem
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
props.onScan();
|
||||
}}
|
||||
>
|
||||
<Scan />
|
||||
{i18n.t("common.scan")}
|
||||
</FabMenuItem>
|
||||
</li>
|
||||
</ul>
|
||||
</FabMenu>
|
||||
</Show>
|
||||
<div class="fixed bottom-8 right-8 text-m-red">
|
||||
<button id="fab" onClick={() => setOpen(!open())}>
|
||||
<Circle size="large">
|
||||
<Plus class="h-8 w-8" />
|
||||
</Circle>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function MiniFab(props: {
|
||||
onSend: () => void;
|
||||
onRequest: () => void;
|
||||
onScan: () => void;
|
||||
sendDisabled?: boolean | undefined;
|
||||
}) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={open()}>
|
||||
<FabMenu setOpen={setOpen} left>
|
||||
<ul class="flex flex-col divide-y divide-m-grey-400/25">
|
||||
<li>
|
||||
<FabMenuItem
|
||||
disabled={props.sendDisabled || false}
|
||||
onClick={() => {
|
||||
props.onSend();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<ArrowUpRight />
|
||||
{i18n.t("common.send")}
|
||||
</FabMenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<FabMenuItem
|
||||
onClick={() => {
|
||||
props.onRequest();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<ArrowDownLeft />
|
||||
{i18n.t("common.request")}
|
||||
</FabMenuItem>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<FabMenuItem
|
||||
onClick={() => {
|
||||
props.onScan();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Scan />
|
||||
{i18n.t("common.scan")}
|
||||
</FabMenuItem>
|
||||
</li>
|
||||
</ul>
|
||||
</FabMenu>
|
||||
</Show>
|
||||
<button id="fab" onClick={() => setOpen(true)}>
|
||||
<Plus class="h-8 w-8 text-m-red" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function Fee(props: { amountSats?: bigint | number }) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FeesModal icon />
|
||||
<FeesModal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
193
src/components/GenericItem.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Check, Clock4, EyeOff, Globe, X, Zap } from "lucide-solid";
|
||||
import { JSX, Match, Show, Switch } from "solid-js";
|
||||
|
||||
import { LabelCircle, LoadingSpinner } from "~/components";
|
||||
|
||||
export function GenericItem(props: {
|
||||
primaryAvatarUrl?: string;
|
||||
icon?: JSX.Element;
|
||||
secondaryAvatarUrl?: string;
|
||||
primaryName: string;
|
||||
secondaryName?: string;
|
||||
verb?: string;
|
||||
amount?: bigint;
|
||||
date?: string;
|
||||
due?: string;
|
||||
message?: string;
|
||||
accent?: "green";
|
||||
visibility?: "public" | "private";
|
||||
showFiat?: boolean;
|
||||
genericAvatar?: boolean;
|
||||
forceSecondary?: boolean;
|
||||
link?: string;
|
||||
primaryOnClick?: () => void;
|
||||
amountOnClick?: () => void;
|
||||
secondaryOnClick?: () => void;
|
||||
approveAction?: () => void;
|
||||
rejectAction?: () => void;
|
||||
shouldSpinny?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="grid w-full py-3 first-of-type:pt-0"
|
||||
classList={{
|
||||
"grid-cols-[auto_1fr_auto]": true,
|
||||
"opacity-50": props.shouldSpinny
|
||||
}}
|
||||
>
|
||||
<div class="self-center">
|
||||
<Switch>
|
||||
<Match when={props.icon}>
|
||||
<button
|
||||
class="flex h-[3rem] w-[3rem] items-center justify-center"
|
||||
onClick={() => props.primaryOnClick}
|
||||
>
|
||||
{props.icon}
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<LabelCircle
|
||||
label={false}
|
||||
name={props.primaryName}
|
||||
contact
|
||||
image_url={props.primaryAvatarUrl}
|
||||
generic={props.genericAvatar}
|
||||
onClick={props.primaryOnClick}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="flex flex-col items-start justify-center gap-1 self-center px-2">
|
||||
{/* TITLE TEXT */}
|
||||
<Show when={props.primaryName && props.verb}>
|
||||
<h2 class="text-sm">
|
||||
<strong
|
||||
classList={{
|
||||
"text-m-grey-400": props.genericAvatar
|
||||
}}
|
||||
>
|
||||
{props.primaryName}
|
||||
</strong>
|
||||
<span class="font-light">{` ${props.verb} `}</span>
|
||||
<Show when={props.secondaryName}>
|
||||
<strong>{props.secondaryName}</strong>
|
||||
</Show>
|
||||
</h2>
|
||||
</Show>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{/* AMOUNT */}
|
||||
<Show when={props.amount}>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.amountOnClick && props.amountOnClick()
|
||||
}
|
||||
class="flex items-center gap-1 rounded-full px-2 py-1 text-xs font-semibold text-white"
|
||||
classList={{
|
||||
"bg-m-grey-800": !props.accent,
|
||||
"bg-m-green/40 ": props.accent === "green"
|
||||
}}
|
||||
>
|
||||
{/* <img src={bolt} width={8} height={8} /> */}
|
||||
<Zap class="w-3" fill="currentColor" />
|
||||
{`${props.amount!.toLocaleString()} sats`}
|
||||
</button>
|
||||
</Show>
|
||||
{/* FIAT AMOUNT */}
|
||||
<Show when={props.showFiat}>
|
||||
<div class="flex items-center gap-1 rounded-full py-1 text-xs font-semibold text-m-grey-400">
|
||||
{`~$42.00 USD`}
|
||||
</div>
|
||||
</Show>
|
||||
{/* OPTIONAL MESSAGE */}
|
||||
<Show when={props.message}>
|
||||
<div class="font-regular line-clamp-1 min-w-0 break-all rounded-full bg-m-grey-800 px-2 py-1 text-xs leading-6">
|
||||
{props.message}
|
||||
</div>
|
||||
</Show>
|
||||
{/* DUE */}
|
||||
<Show when={props.due}>
|
||||
<div class="flex w-full items-center gap-1 text-m-grey-400">
|
||||
<Clock4 class="w-3" />
|
||||
<span class="text-xs text-m-grey-400">
|
||||
{props.due}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
{/* DATE WITH SECOND AVATAR */}
|
||||
<Show when={props.date}>
|
||||
<div class="flex items-center gap-1 text-m-grey-400">
|
||||
<Show when={props.visibility === "public"}>
|
||||
{/* <img src={globe} width={12} height={12} /> */}
|
||||
<Globe class="w-3" />
|
||||
</Show>
|
||||
<Show when={props.visibility === "private"}>
|
||||
<EyeOff class="w-3" />
|
||||
{/* <img src={privateEye} width={12} height={12} /> */}
|
||||
</Show>
|
||||
<Show when={props.link && props.date}>
|
||||
<a
|
||||
href={props.link}
|
||||
class="text-xs text-m-grey-400"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{props.date}
|
||||
</a>
|
||||
</Show>
|
||||
<Show when={!props.link && props.date}>
|
||||
<span class="text-xs text-m-grey-400">
|
||||
{props.date}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.secondaryAvatarUrl || props.forceSecondary}>
|
||||
<div class="self-center">
|
||||
<LabelCircle
|
||||
label={false}
|
||||
name={props.secondaryName}
|
||||
contact
|
||||
image_url={props.secondaryAvatarUrl}
|
||||
onClick={props.secondaryOnClick}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!props.secondaryAvatarUrl && !props.forceSecondary}>
|
||||
<div class="self-center">
|
||||
{/* ACTIONS */}
|
||||
<Show
|
||||
when={
|
||||
props.approveAction &&
|
||||
props.rejectAction &&
|
||||
!props.shouldSpinny
|
||||
}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
class="flex h-10 w-10 items-center justify-center rounded bg-m-grey-800 p-1 text-m-green active:-mb-[1px] active:mt-[1px]"
|
||||
onClick={() =>
|
||||
props.approveAction && props.approveAction()
|
||||
}
|
||||
>
|
||||
<Check />
|
||||
</button>
|
||||
<button
|
||||
class="flex h-10 w-10 items-center justify-center rounded bg-m-grey-800 p-1 text-m-red active:-mb-[1px] active:mt-[1px]"
|
||||
onClick={() =>
|
||||
props.rejectAction && props.rejectAction()
|
||||
}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.shouldSpinny}>
|
||||
<LoadingSpinner wide small />
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { A, useLocation } from "@solidjs/router";
|
||||
import { Gift } from "lucide-solid";
|
||||
|
||||
import gift from "~/assets/icons/gift.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function GiftLink() {
|
||||
@@ -16,7 +16,7 @@ export function GiftLink() {
|
||||
}}
|
||||
>
|
||||
{i18n.t("settings.gift.give_sats_link")}
|
||||
<img src={gift} class="h-5 w-5" alt="Gift" />
|
||||
<Gift class="h-5 w-5" />
|
||||
</A>
|
||||
);
|
||||
}
|
||||
|
||||
54
src/components/HomeBalance.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Match, Switch } from "solid-js";
|
||||
|
||||
import { AmountFiat, AmountSats } from "~/components/Amount";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function HomeBalance() {
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
const lightningPlusFedi = () =>
|
||||
(state.balance?.federation || 0n) + (state.balance?.lightning || 0n);
|
||||
|
||||
// TODO: do some sort of status indicator
|
||||
// const fullyReady = () => state.load_stage === "done" && state.price !== 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={actions.cycleBalanceView}
|
||||
class="flex h-12 items-center justify-center rounded-lg border-b border-t border-b-white/10 border-t-white/40 bg-black px-4 py-2"
|
||||
>
|
||||
{/* <div class="w-2">
|
||||
<div
|
||||
title={fullyReady() ? "READY" : "ALMOST"}
|
||||
class="h-2 w-2 animate-throb rounded-full border-2"
|
||||
classList={{
|
||||
"border-m-green bg-m-green": fullyReady(),
|
||||
"border-m-yellow bg-m-yellow": !fullyReady()
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
<h1 class="flex w-full justify-center whitespace-nowrap text-2xl font-light text-white">
|
||||
<Switch>
|
||||
<Match when={state.balanceView === "sats"}>
|
||||
<AmountSats
|
||||
amountSats={lightningPlusFedi()}
|
||||
icon="lightning"
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={state.balanceView === "fiat"}>
|
||||
<AmountFiat
|
||||
amountSats={lightningPlusFedi()}
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={state.balanceView === "hidden"}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>*****</span>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</h1>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
128
src/components/HomeSubnav.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useSearchParams } from "@solidjs/router";
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
Show,
|
||||
Suspense
|
||||
} from "solid-js";
|
||||
|
||||
import {
|
||||
CombinedActivity,
|
||||
LoadingShimmer,
|
||||
NostrActivity,
|
||||
PendingNwc,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function HomeSubnav() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const [params] = useSearchParams<{
|
||||
tab: "me" | "everybody" | "requests";
|
||||
}>();
|
||||
|
||||
const [activeView, setActiveView] = createSignal<
|
||||
"me" | "everybody" | "requests"
|
||||
>(params.tab || "me");
|
||||
|
||||
const [pending, { refetch }] = createResource(async () => {
|
||||
try {
|
||||
const pending =
|
||||
await state.mutiny_wallet?.get_pending_nwc_invoices();
|
||||
return pending?.length || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (state.is_syncing) {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded px-2 py-1 text-sm"
|
||||
classList={{
|
||||
"bg-m-red": activeView() === "me",
|
||||
"bg-m-grey-800 text-m-grey-400": activeView() !== "me"
|
||||
}}
|
||||
onClick={() => setActiveView("me")}
|
||||
>
|
||||
{i18n.t("home.subnav.just_me")}
|
||||
</button>
|
||||
<button
|
||||
class="rounded px-2 py-1 text-sm"
|
||||
classList={{
|
||||
"bg-m-red": activeView() === "everybody",
|
||||
"bg-m-grey-800 text-m-grey-400":
|
||||
activeView() !== "everybody"
|
||||
}}
|
||||
onClick={() => setActiveView("everybody")}
|
||||
>
|
||||
{i18n.t("home.subnav.friends")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex items-center gap-1 rounded px-2 py-1 text-sm"
|
||||
classList={{
|
||||
"bg-m-red": activeView() === "requests",
|
||||
"bg-m-grey-800 text-m-grey-400":
|
||||
activeView() !== "requests"
|
||||
}}
|
||||
onClick={() => setActiveView("requests")}
|
||||
>
|
||||
<span>{i18n.t("home.subnav.requests")}</span>
|
||||
<Suspense fallback={<></>}>
|
||||
<Show when={pending.latest && pending.latest > 0}>
|
||||
<span
|
||||
class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-xs"
|
||||
classList={{
|
||||
"text-white": !!((pending.latest || 0) > 0)
|
||||
}}
|
||||
>
|
||||
{pending()}
|
||||
</span>
|
||||
</Show>
|
||||
</Suspense>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={activeView() === "me"}>
|
||||
<VStack>
|
||||
<Suspense>
|
||||
<Show
|
||||
when={!state.wallet_loading}
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<CombinedActivity />
|
||||
</Show>
|
||||
</Suspense>
|
||||
</VStack>
|
||||
</Show>
|
||||
<Show when={activeView() === "everybody"}>
|
||||
<Suspense fallback={<LoadingShimmer />}>
|
||||
<NostrActivity />
|
||||
</Suspense>
|
||||
</Show>
|
||||
<Show when={activeView() === "requests"}>
|
||||
<Suspense fallback={<LoadingShimmer />}>
|
||||
<Show when={!state.wallet_loading && !state.safe_mode}>
|
||||
<PendingNwc />
|
||||
</Show>
|
||||
</Suspense>
|
||||
</Show>
|
||||
{/* spacer just so we can always scroll above the fab */}
|
||||
<div class="h-[4rem]" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { X } from "lucide-solid";
|
||||
import { Show } from "solid-js";
|
||||
|
||||
import close from "~/assets/icons/black-close.svg";
|
||||
import { ButtonLink } from "~/components";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
@@ -40,7 +40,7 @@ export function IOSbanner() {
|
||||
onClick={closeBanner}
|
||||
class="self-center justify-self-center rounded-lg hover:bg-white/10 active:bg-m-blue"
|
||||
>
|
||||
<img src={close} alt="Close" class="h-8 w-8" />
|
||||
<X class="h-8 w-8 text-black" />
|
||||
</button>{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Info } from "lucide-solid";
|
||||
import { ParentComponent } from "solid-js";
|
||||
|
||||
import info from "~/assets/icons/info.svg";
|
||||
|
||||
export const InfoBox: ParentComponent<{
|
||||
accent: "red" | "blue" | "green" | "white";
|
||||
}> = (props) => {
|
||||
@@ -16,7 +15,7 @@ export const InfoBox: ParentComponent<{
|
||||
}}
|
||||
>
|
||||
<div class="self-center">
|
||||
<img src={info} alt="info" class="h-8 w-8" />
|
||||
<Info class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm font-light">{props.children}</p>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { Copy, Link, Share, Zap } from "lucide-solid";
|
||||
import { Match, Show, Switch } from "solid-js";
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
|
||||
import boltBlack from "~/assets/icons/bolt-black.svg";
|
||||
import chainBlack from "~/assets/icons/chain-black.svg";
|
||||
import copyBlack from "~/assets/icons/copy-black.svg";
|
||||
import shareBlack from "~/assets/icons/share-black.svg";
|
||||
import { AmountFiat, AmountSats, TruncateMiddle } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { ReceiveFlavor } from "~/routes/Receive";
|
||||
import { useCopy } from "~/utils";
|
||||
|
||||
function KindIndicator(props: { kind: ReceiveFlavor | "gift" }) {
|
||||
function KindIndicator(props: { kind: ReceiveFlavor | "gift" | "lnAddress" }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<div class="flex flex-col items-end text-black">
|
||||
@@ -19,21 +16,28 @@ function KindIndicator(props: { kind: ReceiveFlavor | "gift" }) {
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.onchain")}
|
||||
</h3>
|
||||
<img src={chainBlack} alt="chain" />
|
||||
<Link class="h-4 w-4" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "lightning"}>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.lightning")}
|
||||
</h3>
|
||||
<img src={boltBlack} alt="bolt" />
|
||||
<Zap class="h-4 w-4" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "lnAddress"}>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("contacts.ln_address")}
|
||||
</h3>
|
||||
<Zap class="h-4 w-4" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "gift"}>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.gift")}
|
||||
</h3>
|
||||
<img src={boltBlack} alt="bolt" />
|
||||
<Zap class="h-4 w-4" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "unified"}>
|
||||
@@ -41,8 +45,8 @@ function KindIndicator(props: { kind: ReceiveFlavor | "gift" }) {
|
||||
{i18n.t("receive.integrated_qr.unified")}
|
||||
</h3>
|
||||
<div class="flex gap-1">
|
||||
<img src={chainBlack} alt="chain" />
|
||||
<img src={boltBlack} alt="bolt" />
|
||||
<Zap class="h-4 w-4" />
|
||||
<Link class="h-4 w-4" />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -68,41 +72,46 @@ async function share(receiveString: string) {
|
||||
|
||||
export function IntegratedQr(props: {
|
||||
value: string;
|
||||
amountSats: string;
|
||||
kind: ReceiveFlavor | "gift";
|
||||
kind: ReceiveFlavor | "gift" | "lnAddress";
|
||||
amountSats?: string;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
return (
|
||||
<div
|
||||
id="qr"
|
||||
class="relative flex w-full flex-col items-center rounded-xl bg-white px-4"
|
||||
class="relative flex w-full flex-col items-center rounded-xl bg-white px-4 text-black"
|
||||
onClick={() => copy(props.value)}
|
||||
>
|
||||
<Show when={copied()}>
|
||||
<div class="absolute z-50 flex h-full w-full flex-col items-center justify-center rounded-xl bg-neutral-900/60 transition-all">
|
||||
<div class="absolute z-50 flex h-full w-full flex-col items-center justify-center rounded-xl bg-neutral-900/60 text-white transition-all">
|
||||
<p class="text-xl font-bold">{i18n.t("common.copied")}</p>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex w-full max-w-[256px] items-center py-4"
|
||||
classList={{
|
||||
"justify-between": props.kind !== "onchain",
|
||||
"justify-end": props.kind === "onchain"
|
||||
}}
|
||||
>
|
||||
<Show when={props.kind !== "onchain"}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-black">
|
||||
<Show when={props.kind !== "lnAddress"}>
|
||||
<div
|
||||
class="flex w-full max-w-[256px] items-center py-4"
|
||||
classList={{
|
||||
"justify-between": props.kind !== "onchain",
|
||||
"justify-end": props.kind === "onchain"
|
||||
}}
|
||||
>
|
||||
<Show when={props.kind !== "onchain"}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<AmountSats amountSats={Number(props.amountSats)} />
|
||||
<div class="text-sm ">
|
||||
<AmountFiat
|
||||
amountSats={Number(props.amountSats)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-black">
|
||||
<AmountFiat amountSats={Number(props.amountSats)} />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<KindIndicator kind={props.kind} />
|
||||
</div>
|
||||
</Show>
|
||||
<KindIndicator kind={props.kind} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.kind === "lnAddress"}>
|
||||
<div class="py-4" />
|
||||
</Show>
|
||||
|
||||
<QRCodeSVG
|
||||
value={props.value}
|
||||
@@ -120,18 +129,20 @@ export function IntegratedQr(props: {
|
||||
class="justify-self-start"
|
||||
onClick={(_) => share(props.value)}
|
||||
>
|
||||
<img src={shareBlack} alt="share" />
|
||||
<Share />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={props.kind !== "lnAddress"}>
|
||||
<div class="">
|
||||
<TruncateMiddle text={props.value} whiteBg />
|
||||
</div>
|
||||
<button
|
||||
class=" justify-self-end"
|
||||
onClick={() => copy(props.value)}
|
||||
>
|
||||
<Copy />
|
||||
</button>
|
||||
</Show>
|
||||
<div class="">
|
||||
<TruncateMiddle text={props.value} whiteBg />
|
||||
</div>
|
||||
<button
|
||||
class=" justify-self-end"
|
||||
onClick={() => copy(props.value)}
|
||||
>
|
||||
<img src={copyBlack} alt="copy" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
import { createResource, createSignal, Match, Switch } from "solid-js";
|
||||
import { createResource, createSignal, JSX, Match, Switch } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
import off from "~/assets/icons/download-channel.svg";
|
||||
import on from "~/assets/icons/upload-channel.svg";
|
||||
import { HackActivityType } from "~/components";
|
||||
import avatar from "~/assets/generic-avatar.jpg";
|
||||
import { generateGradient } from "~/utils";
|
||||
|
||||
export function Circle(props: {
|
||||
children: JSX.Element;
|
||||
color?: "red" | "green" | "blue";
|
||||
size?: "small" | "large" | "xl";
|
||||
background?: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Dynamic
|
||||
component={props.onClick ? "button" : "div"}
|
||||
onClick={props.onClick}
|
||||
class="flex flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-3xl uppercase"
|
||||
classList={{
|
||||
"bg-m-grey-800": !props.color && !props.background,
|
||||
"bg-m-red": props.color === "red" && !props.background,
|
||||
"bg-m-green": props.color === "green" && !props.background,
|
||||
"h-[3rem] w-[3rem]": !props.size,
|
||||
"h-[4rem] w-[4rem]": props.size === "large",
|
||||
"h-[8rem] w-[8rem]": props.size === "xl",
|
||||
"active:mt-[1px] active:-mb-[1px]": !!props.onClick
|
||||
}}
|
||||
style={{
|
||||
background: props.background
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Dynamic>
|
||||
);
|
||||
}
|
||||
|
||||
export function LabelCircle(props: {
|
||||
name?: string;
|
||||
image_url?: string;
|
||||
contact: boolean;
|
||||
label: boolean;
|
||||
channel?: HackActivityType;
|
||||
generic?: boolean;
|
||||
size?: "small" | "large" | "xl";
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const [gradient] = createResource(async () => {
|
||||
if (props.name && props.contact) {
|
||||
@@ -31,11 +62,10 @@ export function LabelCircle(props: {
|
||||
const [errored, setErrored] = createSignal(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="flex h-[3rem] w-[3rem] flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 bg-neutral-700 text-3xl uppercase"
|
||||
style={{
|
||||
background: props.image_url && !errored() ? "none" : bg()
|
||||
}}
|
||||
<Circle
|
||||
background={props.image_url && !errored() ? "none" : bg()}
|
||||
onClick={() => props.onClick && props.onClick()}
|
||||
size={props.size}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={errored()}>{text()}</Match>
|
||||
@@ -50,14 +80,11 @@ export function LabelCircle(props: {
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.channel === "ChannelOpen"}>
|
||||
<img src={on} alt="channel open" />
|
||||
</Match>
|
||||
<Match when={props.channel === "ChannelClose"}>
|
||||
<img src={off} alt="channel close" />
|
||||
<Match when={text() === "?" || props.generic}>
|
||||
<img src={avatar} alt="avatar" />
|
||||
</Match>
|
||||
<Match when={true}>{text()}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Circle>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ function LoadingBar(props: { value: number; max: number }) {
|
||||
case 2:
|
||||
return i18n.t("modals.loading.downloading");
|
||||
case 3:
|
||||
return i18n.t("modals.loading.setup");
|
||||
return i18n.t("modals.loading.existing_wallet");
|
||||
case 4:
|
||||
return i18n.t("modals.loading.setup");
|
||||
case 5:
|
||||
return i18n.t("modals.loading.done");
|
||||
default:
|
||||
return i18n.t("modals.loading.default");
|
||||
@@ -51,10 +53,12 @@ export function LoadingIndicator() {
|
||||
return 1;
|
||||
case "downloading":
|
||||
return 2;
|
||||
case "setup":
|
||||
case "checking_for_existing_wallet":
|
||||
return 3;
|
||||
case "done":
|
||||
case "setup":
|
||||
return 4;
|
||||
case "done":
|
||||
return 5;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -62,7 +66,7 @@ export function LoadingIndicator() {
|
||||
|
||||
return (
|
||||
<Show when={state.load_stage !== "done"}>
|
||||
<LoadingBar value={loadStageValue()} max={4} />
|
||||
<LoadingBar value={loadStageValue()} max={5} />
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { createSignal, JSXElement, ParentComponent } from "solid-js";
|
||||
|
||||
import help from "~/assets/icons/help.svg";
|
||||
import { ExternalLink, SimpleDialog } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function FeesModal(props: { icon?: boolean }) {
|
||||
export function FeesModal() {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<MoreInfoModal
|
||||
title={i18n.t("modals.more_info.whats_with_the_fees")}
|
||||
linkText={
|
||||
props.icon ? (
|
||||
<img src={help} alt="help" class="h-4 w-4 cursor-pointer" />
|
||||
) : (
|
||||
i18n.t("common.why")
|
||||
)
|
||||
}
|
||||
linkText={i18n.t("common.why")}
|
||||
>
|
||||
<p>{i18n.t("modals.more_info.self_custodial")}</p>
|
||||
<p>{i18n.t("modals.more_info.future_payments")}</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { ChevronRight } from "lucide-solid";
|
||||
import { ParentComponent, Show } from "solid-js";
|
||||
|
||||
import forward from "~/assets/icons/forward.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
@@ -32,7 +32,7 @@ export function MutinyPlusCta() {
|
||||
{i18n.t("common.mutiny")}
|
||||
<span class="text-m-red">+</span>
|
||||
</span>
|
||||
<img src={forward} alt="go" />
|
||||
<ChevronRight />
|
||||
</div>
|
||||
<div class="text-sm text-m-grey-400">
|
||||
<Show
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { A } from "@solidjs/router";
|
||||
|
||||
import airplane from "~/assets/icons/airplane.svg";
|
||||
import receive from "~/assets/icons/big-receive.svg";
|
||||
import mutiny_m from "~/assets/icons/m.svg";
|
||||
import scan from "~/assets/icons/scan.svg";
|
||||
import settings from "~/assets/icons/settings.svg";
|
||||
import userClock from "~/assets/icons/user-clock.svg";
|
||||
import {
|
||||
ArrowDownLeft,
|
||||
ArrowUpRight,
|
||||
Scan,
|
||||
Settings,
|
||||
User,
|
||||
Wallet
|
||||
} from "lucide-solid";
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
type ActiveTab =
|
||||
| "home"
|
||||
@@ -13,14 +15,13 @@ type ActiveTab =
|
||||
| "send"
|
||||
| "receive"
|
||||
| "settings"
|
||||
| "activity"
|
||||
| "profile"
|
||||
| "none";
|
||||
|
||||
function NavBarItem(props: {
|
||||
href: string;
|
||||
icon: string;
|
||||
icon: JSX.Element;
|
||||
active: boolean;
|
||||
alt: string;
|
||||
}) {
|
||||
return (
|
||||
<li>
|
||||
@@ -28,11 +29,12 @@ function NavBarItem(props: {
|
||||
class="block rounded-lg p-2"
|
||||
href={props.href}
|
||||
classList={{
|
||||
"hover:bg-white/5 active:bg-m-blue": !props.active,
|
||||
"bg-black": props.active
|
||||
"hover:bg-white/20 active:bg-white/10 active:mt-[2px] active:-mb-[2px]":
|
||||
!props.active,
|
||||
"bg-m-red": props.active
|
||||
}}
|
||||
>
|
||||
<img src={props.icon} alt={props.alt} height={36} width={36} />
|
||||
{props.icon}
|
||||
</A>
|
||||
</li>
|
||||
);
|
||||
@@ -44,39 +46,33 @@ export function NavBar(props: { activeTab: ActiveTab }) {
|
||||
<ul class="mt-4 flex flex-col justify-start gap-4 px-4">
|
||||
<NavBarItem
|
||||
href="/"
|
||||
icon={mutiny_m}
|
||||
icon={<Wallet class="h-8 w-8" />}
|
||||
active={props.activeTab === "home"}
|
||||
alt="home"
|
||||
/>
|
||||
<NavBarItem
|
||||
href="/search"
|
||||
icon={airplane}
|
||||
icon={<ArrowUpRight class="h-8 w-8" />}
|
||||
active={props.activeTab === "send"}
|
||||
alt="send"
|
||||
/>
|
||||
<NavBarItem
|
||||
href="/receive"
|
||||
icon={receive}
|
||||
icon={<ArrowDownLeft class="h-8 w-8" />}
|
||||
active={props.activeTab === "receive"}
|
||||
alt="receive"
|
||||
/>
|
||||
<NavBarItem
|
||||
href="/activity"
|
||||
icon={userClock}
|
||||
active={props.activeTab === "activity"}
|
||||
alt="activity"
|
||||
/>
|
||||
<NavBarItem
|
||||
href="/scanner"
|
||||
icon={scan}
|
||||
icon={<Scan class="h-8 w-8" />}
|
||||
active={false}
|
||||
alt="scan"
|
||||
/>
|
||||
<NavBarItem
|
||||
href="/profile"
|
||||
icon={<User class="h-8 w-8" />}
|
||||
active={props.activeTab === "profile"}
|
||||
/>
|
||||
<NavBarItem
|
||||
href="/settings"
|
||||
icon={settings}
|
||||
icon={<Settings class="h-8 w-8" />}
|
||||
active={props.activeTab === "settings"}
|
||||
alt="settings"
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Search } from "lucide-solid";
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
@@ -7,13 +10,14 @@ import {
|
||||
Switch
|
||||
} from "solid-js";
|
||||
|
||||
import rightArrow from "~/assets/icons/right-arrow.svg";
|
||||
import { AmountSats, VStack } from "~/components";
|
||||
import { ButtonCard, NiceP } from "~/components/layout";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { fetchZaps, hexpubFromNpub } from "~/utils";
|
||||
import { fetchZaps, getPrimalImageUrl } from "~/utils";
|
||||
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||
|
||||
import { GenericItem } from "./GenericItem";
|
||||
|
||||
export function Avatar(props: { image_url?: string; large?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
@@ -32,18 +36,12 @@ export function Avatar(props: { image_url?: string; large?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatProfileLink(hexpub: string): string {
|
||||
return `https://primal.net/p/${hexpub}`;
|
||||
}
|
||||
|
||||
export function NostrActivity() {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const [data, { refetch }] = createResource(state.npub, fetchZaps);
|
||||
|
||||
const [userHexpub] = createResource(state.npub, hexpubFromNpub);
|
||||
|
||||
function nameFromHexpub(hexpub: string): string {
|
||||
const profile = data.latest?.profiles[hexpub];
|
||||
if (!profile) return hexpub;
|
||||
@@ -56,8 +54,8 @@ export function NostrActivity() {
|
||||
const profile = data.latest?.profiles[hexpub];
|
||||
if (!profile) return;
|
||||
const parsed = JSON.parse(profile.content);
|
||||
const image_url = parsed.picture;
|
||||
return image_url;
|
||||
const image_url = parsed.image || parsed.picture;
|
||||
return getPrimalImageUrl(image_url);
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@@ -67,105 +65,108 @@ export function NostrActivity() {
|
||||
}
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// TODO: can this be part of mutiny wallet?
|
||||
async function newContactFromHexpub(hexpub: string) {
|
||||
try {
|
||||
const npub = await MutinyWallet.hexpub_to_npub(hexpub);
|
||||
|
||||
if (!npub) {
|
||||
throw new Error("No npub for that hexpub");
|
||||
}
|
||||
|
||||
const existingContact =
|
||||
await state.mutiny_wallet?.get_contact_for_npub(npub);
|
||||
|
||||
if (existingContact) {
|
||||
navigate(`/chat/${existingContact.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = data.latest?.profiles[hexpub];
|
||||
if (!profile) return;
|
||||
const parsed = JSON.parse(profile.content);
|
||||
const name = parsed.display_name || parsed.name || profile.pubkey;
|
||||
const image_url = parsed.image || parsed.picture || undefined;
|
||||
const ln_address = parsed.lud16 || undefined;
|
||||
const lnurl = parsed.lud06 || undefined;
|
||||
|
||||
const contactId = await state.mutiny_wallet?.create_new_contact(
|
||||
name,
|
||||
npub,
|
||||
ln_address,
|
||||
lnurl,
|
||||
image_url
|
||||
);
|
||||
|
||||
if (!contactId) {
|
||||
throw new Error("no contact id returned");
|
||||
}
|
||||
|
||||
const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
|
||||
|
||||
if (!tagItem) {
|
||||
throw new Error("no contact returned");
|
||||
}
|
||||
|
||||
navigate(`/chat/${contactId}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
|
||||
<Show when={!data.latest || data.latest?.zaps.length === 0}>
|
||||
<ButtonCard onClick={() => navigate("/search")}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.find")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Show>
|
||||
<For each={data.latest?.zaps}>
|
||||
{(zap) => (
|
||||
<div
|
||||
class="rounded-lg bg-m-grey-800 p-2"
|
||||
classList={{
|
||||
"outline outline-m-blue":
|
||||
userHexpub() === zap.to_hexpub
|
||||
}}
|
||||
>
|
||||
<div class="grid grid-cols-[1fr_auto_1fr] gap-4">
|
||||
<div class="grid gap-2 sm:grid-cols-[auto_1fr] sm:items-center">
|
||||
<Avatar
|
||||
image_url={imageFromHexpub(zap.from_hexpub)}
|
||||
/>
|
||||
<span class="truncate whitespace-nowrap text-left text-sm font-semibold uppercase">
|
||||
<Switch>
|
||||
<Match when={zap.kind === "public"}>
|
||||
<a
|
||||
href={formatProfileLink(
|
||||
zap.from_hexpub
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="no-underline"
|
||||
>
|
||||
{nameFromHexpub(
|
||||
zap.from_hexpub
|
||||
)}
|
||||
</a>
|
||||
</Match>
|
||||
<Match when={zap.kind === "private"}>
|
||||
{i18n.t("activity.private")}
|
||||
</Match>
|
||||
<Match when={zap.kind === "anonymous"}>
|
||||
{i18n.t("activity.anonymous")}
|
||||
</Match>
|
||||
</Switch>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="flex items-center gap-1">
|
||||
<AmountSats amountSats={zap.amount_sats} />
|
||||
<img
|
||||
src={rightArrow}
|
||||
alt="right arrow"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<time class="text-sm text-m-grey-400">
|
||||
<Show
|
||||
when={zap.event_id}
|
||||
fallback={timeAgo(
|
||||
zap.timestamp,
|
||||
data.latest?.until
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={`https://primal.net/e/${zap.event_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{timeAgo(
|
||||
zap.timestamp,
|
||||
data.latest?.until
|
||||
)}
|
||||
</a>
|
||||
</Show>
|
||||
</time>
|
||||
</div>
|
||||
<div class="grid gap-2 self-end sm:grid-cols-[1fr_auto] sm:items-center ">
|
||||
<div class="self-right flex justify-end">
|
||||
<Avatar
|
||||
image_url={imageFromHexpub(
|
||||
zap.to_hexpub
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href={formatProfileLink(zap.to_hexpub)}
|
||||
class="truncate whitespace-nowrap text-right text-sm font-semibold uppercase no-underline sm:-order-1 sm:text-right"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{nameFromHexpub(zap.to_hexpub)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={zap.content}>
|
||||
<hr class="my-2 border-m-grey-750" />
|
||||
<p
|
||||
class="truncate text-center text-sm font-light text-neutral-200"
|
||||
textContent={zap.content}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<>
|
||||
<GenericItem
|
||||
primaryAvatarUrl={
|
||||
imageFromHexpub(zap.from_hexpub) || ""
|
||||
}
|
||||
primaryName={
|
||||
zap.kind === "anonymous"
|
||||
? i18n.t("activity.anonymous")
|
||||
: zap.kind === "private"
|
||||
? i18n.t("activity.private")
|
||||
: nameFromHexpub(zap.from_hexpub)
|
||||
}
|
||||
primaryOnClick={() => {
|
||||
newContactFromHexpub(zap.from_hexpub);
|
||||
}}
|
||||
secondaryAvatarUrl={
|
||||
imageFromHexpub(zap.to_hexpub) || ""
|
||||
}
|
||||
secondaryName={nameFromHexpub(zap.to_hexpub)}
|
||||
secondaryOnClick={() => {
|
||||
newContactFromHexpub(zap.to_hexpub);
|
||||
}}
|
||||
verb={"zapped"}
|
||||
amount={zap.amount_sats}
|
||||
message={zap.content ? zap.content : undefined}
|
||||
date={timeAgo(zap.timestamp, data.latest?.until)}
|
||||
visibility={
|
||||
zap.kind === "public" ? "public" : "private"
|
||||
}
|
||||
genericAvatar={
|
||||
zap.kind === "anonymous" ||
|
||||
zap.kind === "private"
|
||||
}
|
||||
forceSecondary
|
||||
link={`https://njump.me/e/${zap.event_id}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Show } from "solid-js";
|
||||
|
||||
import save from "~/assets/icons/save.svg";
|
||||
import { ButtonLink, SmallHeader } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function OnboardWarning() {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={!state.has_backed_up}>
|
||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] gap-4 rounded-xl bg-neutral-950/50 p-4">
|
||||
<div class="self-center">
|
||||
<img src={save} alt="backup" class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
<SmallHeader>
|
||||
{i18n.t("modals.onboarding.secure_your_funds")}
|
||||
</SmallHeader>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ButtonLink
|
||||
intent="blue"
|
||||
layout="xs"
|
||||
href="/settings/backup"
|
||||
>
|
||||
{i18n.t("settings.backup.title")}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Check, PlugZap, X } from "lucide-solid";
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
@@ -8,22 +11,13 @@ import {
|
||||
Switch
|
||||
} from "solid-js";
|
||||
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import greenCheck from "~/assets/icons/green-check.svg";
|
||||
import redClose from "~/assets/icons/red-close.svg";
|
||||
import {
|
||||
ActivityAmount,
|
||||
Card,
|
||||
InfoBox,
|
||||
LoadingSpinner,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { ButtonCard, GenericItem, InfoBox, NiceP } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import {
|
||||
createDeepSignal,
|
||||
eify,
|
||||
formatExpiration,
|
||||
veryShortTimeStamp,
|
||||
vibrateSuccess
|
||||
} from "~/utils";
|
||||
|
||||
@@ -40,10 +34,16 @@ export function PendingNwc() {
|
||||
|
||||
const [error, setError] = createSignal<Error>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function fetchPendingRequests() {
|
||||
const profiles = await state.mutiny_wallet?.get_nwc_profiles();
|
||||
if (!profiles) return [];
|
||||
|
||||
const contacts: TagItem[] | undefined =
|
||||
await state.mutiny_wallet?.get_contacts_sorted();
|
||||
if (!contacts) return [];
|
||||
|
||||
const pending = await state.mutiny_wallet?.get_pending_nwc_invoices();
|
||||
if (!pending) return [];
|
||||
|
||||
@@ -59,6 +59,16 @@ export function PendingNwc() {
|
||||
date: p.expiry,
|
||||
amount_sats: p.amount_sats
|
||||
});
|
||||
} else {
|
||||
const contact = contacts.find((c) => c.npub === p.npub);
|
||||
if (contact) {
|
||||
pendingItems.push({
|
||||
id: p.id,
|
||||
name_of_connection: contact.name,
|
||||
date: p.expiry,
|
||||
amount_sats: p.amount_sats
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return pendingItems;
|
||||
@@ -70,11 +80,12 @@ export function PendingNwc() {
|
||||
{ storage: createDeepSignal }
|
||||
);
|
||||
|
||||
const [paying, setPaying] = createSignal<string>("");
|
||||
const [payList, setPayList] = createSignal<string[]>([]);
|
||||
|
||||
async function payItem(item: PendingItem) {
|
||||
try {
|
||||
setPaying(item.id);
|
||||
// setPaying(item.id);
|
||||
setPayList([...payList(), item.id]);
|
||||
await state.mutiny_wallet?.approve_invoice(item.id);
|
||||
await vibrateSuccess();
|
||||
} catch (e) {
|
||||
@@ -93,7 +104,7 @@ export function PendingNwc() {
|
||||
console.error(e);
|
||||
}
|
||||
} finally {
|
||||
setPaying("");
|
||||
setPayList(payList().filter((id) => id !== item.id));
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
@@ -119,13 +130,13 @@ export function PendingNwc() {
|
||||
|
||||
async function rejectItem(item: PendingItem) {
|
||||
try {
|
||||
setPaying(item.id);
|
||||
setPayList([...payList(), item.id]);
|
||||
await state.mutiny_wallet?.deny_invoice(item.id);
|
||||
} catch (e) {
|
||||
setError(eify(e));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setPaying("");
|
||||
setPayList(payList().filter((id) => id !== item.id));
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
@@ -147,92 +158,70 @@ export function PendingNwc() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||
<Card title={i18n.t("settings.connections.pending_nwc.title")}>
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Switch>
|
||||
<Match when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||
<ButtonCard onClick={() => navigate("/settings/connections")}>
|
||||
<div class="flex items-center gap-2">
|
||||
<PlugZap class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.connection_edit")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
<div class="flex w-full justify-around">
|
||||
<button
|
||||
class="flex items-center gap-1 font-semibold text-m-green active:-mb-[1px] active:mt-[1px] active:text-m-green/80"
|
||||
onClick={approveAll}
|
||||
>
|
||||
<Check />
|
||||
<span>
|
||||
{i18n.t(
|
||||
"settings.connections.pending_nwc.approve_all"
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1 font-semibold text-m-red active:-mb-[1px] active:mt-[1px] active:text-m-red/80"
|
||||
onClick={denyAll}
|
||||
>
|
||||
<X />
|
||||
<span>
|
||||
{i18n.t(
|
||||
"settings.connections.pending_nwc.deny_all"
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()?.message}</InfoBox>
|
||||
</Show>
|
||||
|
||||
<For each={pendingRequests()}>
|
||||
{(pendingItem) => (
|
||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)_auto] items-center gap-4 border-b border-neutral-800 pb-4 last:border-b-0">
|
||||
<img
|
||||
class="w-[1rem]"
|
||||
src={bolt}
|
||||
alt="onchain"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="truncate text-base font-semibold">
|
||||
{pendingItem.name_of_connection}
|
||||
</span>
|
||||
<time class="text-sm text-neutral-500">
|
||||
{formatExpiration(pendingItem.date)}
|
||||
</time>
|
||||
</div>
|
||||
<div>
|
||||
<ActivityAmount
|
||||
amount={
|
||||
pendingItem.amount_sats?.toString() ||
|
||||
"0"
|
||||
}
|
||||
price={state.price}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex w-[5rem] gap-2">
|
||||
<Switch>
|
||||
<Match
|
||||
when={paying() !== pendingItem.id}
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
payItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2.5rem] w-[2.5rem]"
|
||||
src={greenCheck}
|
||||
alt="Approve"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
rejectItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2rem] w-[2rem]"
|
||||
src={redClose}
|
||||
alt="Reject"
|
||||
/>
|
||||
</button>
|
||||
</Match>
|
||||
<Match
|
||||
when={paying() === pendingItem.id}
|
||||
>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<GenericItem
|
||||
primaryAvatarUrl=""
|
||||
verb="requested"
|
||||
amount={pendingItem.amount_sats || 0n}
|
||||
due={veryShortTimeStamp(pendingItem.date)}
|
||||
genericAvatar
|
||||
primaryName={pendingItem.name_of_connection}
|
||||
approveAction={() => payItem(pendingItem)}
|
||||
rejectAction={() => rejectItem(pendingItem)}
|
||||
shouldSpinny={payList().includes(
|
||||
pendingItem.id
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</VStack>
|
||||
<div class="flex w-full justify-around">
|
||||
<button
|
||||
class="font-semibold text-m-green active:text-m-red/80"
|
||||
onClick={approveAll}
|
||||
>
|
||||
{i18n.t("settings.connections.pending_nwc.approve_all")}
|
||||
</button>
|
||||
<button
|
||||
class="font-semibold text-m-red active:text-m-red/80"
|
||||
onClick={denyAll}
|
||||
>
|
||||
{i18n.t("settings.connections.pending_nwc.deny_all")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<ButtonCard onClick={() => navigate("/settings/connections")}>
|
||||
<div class="flex items-center gap-2">
|
||||
<PlugZap class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("home.connection")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Capacitor } from "@capacitor/core";
|
||||
import QrScanner from "qr-scanner";
|
||||
import { onCleanup, onMount } from "solid-js";
|
||||
|
||||
export function Scanner(props: { onResult: (result: string) => void }) {
|
||||
export function Reader(props: { onResult: (result: string) => void }) {
|
||||
let container: HTMLVideoElement | undefined;
|
||||
let scanner: QrScanner | undefined;
|
||||
|
||||
|
||||