wip: complete new onboarding

This commit is contained in:
reya 2023-10-17 16:33:41 +07:00
parent 3aa4f294f9
commit 7fa1e89dc8
44 changed files with 580 additions and 732 deletions

View File

@ -1,45 +1,29 @@
-- Add migration script here
-- create accounts table -- create accounts table
-- is_active (multi-account feature), value:
-- 0: false
-- 1: true
CREATE TABLE CREATE TABLE
accounts ( accounts (
id INTEGER NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
npub TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL UNIQUE, pubkey TEXT NOT NULL UNIQUE,
privkey TEXT NOT NULL, follows TEXT,
follows JSON, circles TEXT,
is_active INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 0,
last_login_at NUMBER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- create notes table -- create notes table
CREATE TABLE CREATE TABLE
notes ( events (
id INTEGER NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
pubkey TEXT NOT NULL, event TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 1, author TEXT NOT NULL,
tags JSON, kind NUMBER NOT NULL DEFAULt 1,
content TEXT NOT NULL, root_id TEXT,
reply_id TEXT,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
parent_id TEXT,
FOREIGN KEY (account_id) REFERENCES accounts (id) FOREIGN KEY (account_id) REFERENCES accounts (id)
); );
-- create channels table
CREATE TABLE
channels (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
name TEXT,
about TEXT,
picture TEXT,
created_at INTEGER NOT NULL
);
-- create settings table -- create settings table
CREATE TABLE CREATE TABLE
settings ( settings (
@ -49,11 +33,23 @@ CREATE TABLE
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- create metadata table
CREATE TABLE CREATE TABLE
metadata ( widgets (
id TEXT NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
pubkey TEXT NOT NULL, account_id INTEGER NOT NULL,
kind INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
); FOREIGN KEY (account_id) REFERENCES accounts (id)
);
CREATE TABLE
relays (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
relay TEXT NOT NULL UNIQUE,
purpose TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@ -1,12 +0,0 @@
-- Add migration script here
-- create chats table
CREATE TABLE
chats (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
receiver_pubkey INTEGER NOT NULL,
sender_pubkey TEXT NOT NULL,
content TEXT NOT NULL,
tags JSON,
created_at INTEGER NOT NULL
);

View File

@ -1,14 +0,0 @@
-- Add migration script here
INSERT INTO
settings (key, value)
VALUES
("last_login", "0"),
(
"relays",
'["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://relay.nostrgraph.net","wss://nostr.mutinywallet.com"]'
),
("auto_start", "0"),
("cache_time", "86400000"),
("compose_shortcut", "meta+n"),
("add_imageblock_shortcut", "meta+i"),
("add_feedblock_shortcut", "meta+f")

View File

@ -1,3 +0,0 @@
-- Add migration script here
-- add pubkey to channel
ALTER TABLE channels ADD pubkey TEXT NOT NULL DEFAULT '';

View File

@ -1,38 +0,0 @@
-- Add migration script here
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753",
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
"lume-general",
"General channel for Lume",
"https://void.cat/d/UNyxBmAh1MUx5gQTX95jyf.webp",
1681898574
);
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
"ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69",
"Nostr",
"",
"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png",
1661333723
);

View File

@ -1,11 +0,0 @@
-- Add migration script here
-- create blacklist table
CREATE TABLE
blacklist (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
content TEXT NOT NULL UNIQUE,
status INTEGER NOT NULL DEFAULT 0,
kind INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@ -1,11 +0,0 @@
-- Add migration script here
CREATE TABLE
blocks (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
kind INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@ -1,15 +0,0 @@
-- Add migration script here
CREATE TABLE
channel_messages (
id INTEGER NOT NULL PRIMARY KEY,
channel_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL,
content TEXT NOT NULL,
tags JSON,
mute BOOLEAN DEFAULT 0,
hide BOOLEAN DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY (channel_id) REFERENCES channels (event_id)
);

View File

@ -1,13 +0,0 @@
-- Add migration script here
CREATE TABLE
replies (
id INTEGER NOT NULL PRIMARY KEY,
parent_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 1,
tags JSON,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (parent_id) REFERENCES notes (event_id)
);

View File

@ -1,6 +0,0 @@
-- Add migration script here
DROP TABLE IF EXISTS blacklist;
DROP TABLE IF EXISTS channel_messages;
DROP TABLE IF EXISTS channels;

View File

@ -1,6 +0,0 @@
-- Add migration script here
UPDATE settings
SET
value = '["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://nostr.mutinywallet.com"]'
WHERE
key = 'relays';

View File

@ -1,2 +0,0 @@
-- Add migration script here
ALTER TABLE accounts ADD network JSON;

View File

@ -1,10 +0,0 @@
-- Add migration script here
CREATE TABLE
relays (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
relay TEXT NOT NULL,
purpose TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@ -1,3 +0,0 @@
-- Add migration script here
ALTER TABLE blocks
RENAME TO widgets;

View File

@ -1,13 +0,0 @@
-- Add migration script here
CREATE TABLE
events (
id TEXT NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
event TEXT NOT NULL,
author TEXT NOT NULL,
kind NUMBER NOT NULL DEFAULt 1,
root_id TEXT,
reply_id TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@ -1,8 +0,0 @@
-- Add migration script here
DROP TABLE IF EXISTS notes;
DROP TABLE IF EXISTS chats;
DROP TABLE IF EXISTS metadata;
DROP TABLE IF EXISTS replies;

View File

@ -1,3 +0,0 @@
-- Add migration script here
ALTER TABLE accounts
ADD COLUMN last_login_at NUMBER NOT NULL DEFAULT 0;

View File

@ -1,2 +0,0 @@
-- Add migration script here
CREATE UNIQUE INDEX unique_relay ON relays (relay);

View File

@ -116,116 +116,12 @@ fn main() {
tauri_plugin_sql::Builder::default() tauri_plugin_sql::Builder::default()
.add_migrations( .add_migrations(
"sqlite:lume_v2.db", "sqlite:lume_v2.db",
vec![ vec![Migration {
Migration { version: 20230418013219,
version: 20230418013219, description: "initial data",
description: "initial data", sql: include_str!("../migrations/20230418013219_initial_data.sql"),
sql: include_str!("../migrations/20230418013219_initial_data.sql"), kind: MigrationKind::Up,
kind: MigrationKind::Up, }],
},
Migration {
version: 20230418080146,
description: "create chats",
sql: include_str!("../migrations/20230418080146_create_chats.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230420040005,
description: "insert last login to settings",
sql: include_str!("../migrations/20230420040005_insert_last_login_to_settings.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425023912,
description: "add pubkey to channel",
sql: include_str!("../migrations/20230425023912_add_pubkey_to_channel.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425024708,
description: "add default channels",
sql: include_str!("../migrations/20230425024708_add_default_channels.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425050745,
description: "create blacklist",
sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230521092300,
description: "create block",
sql: include_str!("../migrations/20230521092300_add_block_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230617003135,
description: "add channel messages",
sql: include_str!("../migrations/20230617003135_add_channel_messages.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230619082415,
description: "add replies",
sql: include_str!("../migrations/20230619082415_add_replies.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230718072634,
description: "clean up",
sql: include_str!("../migrations/20230718072634_clean_up_old_tables.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230725010250,
description: "update default relays",
sql: include_str!("../migrations/20230725010250_update_default_relays.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230804083544,
description: "add network to accounts",
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230808085847,
description: "add relays",
sql: include_str!("../migrations/20230808085847_add_relays_table.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230811074423,
description: "rename blocks to widgets",
sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230814083543,
description: "add events",
sql: include_str!("../migrations/20230814083543_add_events_table.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230816090508,
description: "clean up tables",
sql: include_str!("../migrations/20230816090508_clean_up_tables.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230817014932,
description: "add last login to account",
sql: include_str!("../migrations/20230817014932_add_last_login_time_to_account.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230918235335,
description: "add unique to relay",
sql: include_str!("../migrations/20230918235335_add_uniq_to_relay.sql"),
kind: MigrationKind::Up,
},
],
) )
.build(), .build(),
) )

View File

@ -94,13 +94,6 @@ export default function App() {
return { Component: RelayScreen }; return { Component: RelayScreen };
}, },
}, },
{
path: 'communities',
async lazy() {
const { CommunitiesScreen } = await import('@app/communities');
return { Component: CommunitiesScreen };
},
},
{ {
path: 'explore', path: 'explore',
element: ( element: (
@ -173,13 +166,6 @@ export default function App() {
return { Component: ImportAccountScreen }; return { Component: ImportAccountScreen };
}, },
}, },
{
path: 'complete',
async lazy() {
const { CompleteScreen } = await import('@app/auth/complete');
return { Component: CompleteScreen };
},
},
{ {
path: 'onboarding', path: 'onboarding',
element: <OnboardingScreen />, element: <OnboardingScreen />,
@ -203,6 +189,15 @@ export default function App() {
return { Component: OnboardEnrichScreen }; return { Component: OnboardEnrichScreen };
}, },
}, },
{
path: 'hashtag',
async lazy() {
const { OnboardHashtagScreen } = await import(
'@app/auth/onboarding/hashtag'
);
return { Component: OnboardHashtagScreen };
},
},
], ],
}, },
], ],

View File

@ -1,42 +0,0 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
export function CompleteScreen() {
const navigate = useNavigate();
const [count, setCount] = useState(5);
useEffect(() => {
let counter: NodeJS.Timeout;
if (count > 0) {
counter = setTimeout(() => setCount(count - 1), 1000);
}
if (count === 0) {
navigate('/', { replace: true });
}
return () => {
clearTimeout(counter);
};
}, [count]);
return (
<div className="relative flex h-full w-full flex-col items-center justify-center">
<div className="mx-auto flex max-w-xl flex-col gap-1.5 text-center">
<h1 className="text-2xl font-light leading-none text-white">
<span className="font-semibold">You&apos;re ready</span>, redirecting in {count}
</h1>
<p className="text-white/70">
Thank you for using Lume. Lume doesn&apos;t use telemetry. If you encounter any
problems, please submit a report via the &quot;Report Issue&quot; button.
<br />
You can find it while using the application.
</p>
</div>
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 transform items-center justify-center">
<img src="/lume.png" alt="lume" className="h-auto w-1/5" />
</div>
</div>
);
}

View File

@ -0,0 +1,50 @@
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function AllowNotification() {
const [notification, setNotification] = useOnboarding((state) => [
state.notification,
state.toggleNotification,
]);
const allow = async () => {
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
if (permissionGranted) {
setNotification();
}
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Allow notification</h5>
<p className="text-sm">
By allowing Lume to send notifications in your OS settings, you will receive
notification messages when someone interacts with you or your content.
</p>
</div>
{notification ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={allow}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Allow
</button>
)}
</div>
</div>
);
}

View File

@ -1,21 +0,0 @@
export function CustomRelay() {
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Personalize relay list</h5>
<p className="text-sm">
Lume offers some default relays for users who are not familiar with Nostr, but
you can consider adding more relays to discover more content.
</p>
</div>
<button
type="button"
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Custom
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,99 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { LRUCache } from 'lru-cache';
import { useState } from 'react';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function Circle() {
const { db } = useStorage();
const { ndk } = useNDK();
const [circle, setCircle] = useOnboarding((state) => [
state.circle,
state.toggleCircle,
]);
const [loading, setLoading] = useState(false);
const enableLinks = async () => {
setLoading(true);
const users = ndk.getUser({ hexpubkey: db.account.pubkey });
const follows = await users.follows();
if (follows.size === 0) {
setLoading(false);
return toast('You need to follow at least 1 account');
}
const lru = new LRUCache<string, string, void>({ max: 300 });
const followsAsArr = [];
// add user's follows to lru
follows.forEach((user) => {
lru.set(user.pubkey, user.pubkey);
followsAsArr.push(user.pubkey);
});
// get follows from follows
const events = await ndk.fetchEvents({
kinds: [NDKKind.Contacts],
authors: followsAsArr,
limit: 300,
});
events.forEach((event: NDKEvent) => {
event.tags.forEach((tag) => {
if (tag[0] === 'p') lru.set(tag[1], tag[1]);
});
});
// get lru values
const circleList = [...lru.values()] as string[];
// update db
await db.updateAccount('follows', JSON.stringify(followsAsArr));
await db.updateAccount('circles', JSON.stringify(circleList));
db.account.follows = followsAsArr;
db.account.circles = circleList;
// clear lru
lru.clear();
// done
setCircle();
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Enable Circle</h5>
<p className="text-sm">
Beside newsfeed from your follows, you will see more content from all people
that followed by your follows.
</p>
</div>
{circle ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={enableLinks}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Enable'}
</button>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,47 @@
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function OutboxModel() {
const { db } = useStorage();
const [outbox, setOutbox] = useOnboarding((state) => [
state.outbox,
state.toggleOutbox,
]);
const enableOutbox = async () => {
await db.createSetting('outbox', '1');
setOutbox();
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Enable Outbox (experiment)</h5>
<p className="text-sm">
When you request information about a user, Lume will automatically query the
user&apos;s outbox relays and subsequent queries will favour using those
relays for queries with that user&apos;s pubkey.
</p>
</div>
{outbox ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={enableOutbox}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Enable
</button>
)}
</div>
</div>
);
}

View File

@ -1,4 +1,12 @@
import { Link } from 'react-router-dom';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function FavoriteHashtag() { export function FavoriteHashtag() {
const hashtag = useOnboarding((state) => state.hashtag);
return ( return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"> <div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@ -9,12 +17,18 @@ export function FavoriteHashtag() {
hashtag as a column hashtag as a column
</p> </p>
</div> </div>
<button {hashtag ? (
type="button" <div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800" <CheckCircleIcon className="h-4 w-4" />
> </div>
Add ) : (
</button> <Link
to="/auth/onboarding/hashtag"
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Add
</Link>
)}
</div> </div>
</div> </div>
); );

View File

@ -1,3 +1,58 @@
import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
export function FollowList() { export function FollowList() {
return <div></div>; const { db } = useStorage();
const { ndk } = useNDK();
const { status, data } = useQuery(
['follows'],
async () => {
const user = ndk.getUser({ hexpubkey: db.account.pubkey });
const follows = await user.follows();
const followsAsArr = [];
follows.forEach((user) => {
followsAsArr.push(user.pubkey);
});
// update db
await db.updateAccount('follows', JSON.stringify(followsAsArr));
await db.updateAccount('circles', JSON.stringify(followsAsArr));
db.account.follows = followsAsArr;
db.account.circles = followsAsArr;
return followsAsArr;
},
{
refetchOnWindowFocus: false,
}
);
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<h5 className="font-semibold">Your follows</h5>
<div className="mt-2 flex w-full items-center justify-center">
{status === 'loading' ? (
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
) : (
<div className="isolate flex -space-x-2">
{data.slice(0, 16).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{data.length > 16 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
<span className="text-xs font-medium">+{data.length}</span>
</div>
) : null}
</div>
)}
</div>
</div>
);
} }

View File

@ -1,21 +0,0 @@
export function LinkList() {
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Enable Links</h5>
<p className="text-sm">
Beside newsfeed from your follows, you will see more content from all people
that followed by your follows.
</p>
</div>
<button
type="button"
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Enable
</button>
</div>
</div>
);
}

View File

@ -1,21 +0,0 @@
export function NIP04() {
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Enable direct message (Deprecated)</h5>
<p className="text-sm">
Send direct message to other user (NIP-04), all messages will be encrypted,
but your metadata will be leaked.
</p>
</div>
<button
type="button"
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Enable
</button>
</div>
</div>
);
}

View File

@ -1,6 +1,12 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function SuggestFollow() { export function SuggestFollow() {
const enrich = useOnboarding((state) => state.enrich);
return ( return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"> <div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@ -11,12 +17,18 @@ export function SuggestFollow() {
world. world.
</p> </p>
</div> </div>
<Link {enrich ? (
to="/auth/onboarding/enrich" <div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800" <CheckCircleIcon className="h-4 w-4" />
> </div>
Check ) : (
</Link> <Link
to="/auth/onboarding/enrich"
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Check
</Link>
)}
</div> </div>
</div> </div>
); );

View File

@ -82,7 +82,7 @@ export function ImportAccountScreen() {
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"> <div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label htmlFor="npub" className="font-semibold"> <label htmlFor="npub" className="font-semibold">
Enter your npub: Enter your public key:
</label> </label>
<div className="inline-flex w-full items-center gap-2"> <div className="inline-flex w-full items-center gap-2">
<input <input
@ -156,7 +156,7 @@ export function ImportAccountScreen() {
> >
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label htmlFor="nsec" className="font-semibold"> <label htmlFor="nsec" className="font-semibold">
Enter your nsec (optional): Enter your private key (optional):
</label> </label>
<div className="inline-flex w-full items-center gap-2"> <div className="inline-flex w-full items-center gap-2">
<input <input
@ -187,12 +187,12 @@ export function ImportAccountScreen() {
</div> </div>
<div className="mt-3 select-text"> <div className="mt-3 select-text">
<p className="text-sm"> <p className="text-sm">
<b>nsec</b> is used to sign your event. For example, if you want to <b>Private Key</b> is used to sign your event. For example, if you
make a new post or send a message to your contact, you need to use want to make a new post or send a message to your contact, you need to
nsec to sign this event. use your private key to sign this event.
</p> </p>
<h5 className="mt-2 font-semibold"> <h5 className="mt-2 font-semibold">
1. In case you store nsec in Lume 1. In case you store private key in Lume
</h5> </h5>
<p className="text-sm"> <p className="text-sm">
Lume will put your nsec to{' '} Lume will put your nsec to{' '}
@ -204,12 +204,12 @@ export function ImportAccountScreen() {
, it will be secured by your OS , it will be secured by your OS
</p> </p>
<h5 className="mt-2 font-semibold"> <h5 className="mt-2 font-semibold">
2. In case you do not store nsec in Lume 2. In case you do not store private key in Lume
</h5> </h5>
<p className="text-sm"> <p className="text-sm">
When you make an event that requires a sign by your nsec, Lume will When you make an event that requires a sign by your private key, Lume
show a prompt popup for you to enter nsec. It will be cleared after will show a prompt for you to enter private key. It will be cleared
signing and not stored anywhere. after signing and not stored anywhere.
</p> </p>
</div> </div>
</motion.div> </motion.div>

View File

@ -1,17 +1,19 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { useOnboarding } from '@stores/onboarding';
import { useNostr } from '@utils/hooks/useNostr'; import { useNostr } from '@utils/hooks/useNostr';
import { arrayToNIP02 } from '@utils/transform'; import { arrayToNIP02 } from '@utils/transform';
export function OnboardEnrichScreen() { export function OnboardEnrichScreen() {
const { publish, fetchUserData } = useNostr();
const { db } = useStorage(); const { db } = useStorage();
const { status, data } = useQuery(['trending-profiles-widget'], async () => { const { status, data } = useQuery(['trending-profiles-widget'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles'); const res = await fetch('https://api.nostr.band/v0/trending/profiles');
@ -20,11 +22,13 @@ export function OnboardEnrichScreen() {
} }
return res.json(); return res.json();
}); });
const { publish } = useNostr();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState([]); const [follows, setFollows] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
const setEnrich = useOnboarding((state) => state.toggleEnrich);
// toggle follow state // toggle follow state
const toggleFollow = (pubkey: string) => { const toggleFollow = (pubkey: string) => {
@ -38,21 +42,24 @@ export function OnboardEnrichScreen() {
try { try {
setLoading(true); setLoading(true);
const tags = arrayToNIP02([...follows, db.account.pubkey]); const tags = arrayToNIP02(follows);
const event = await publish({ content: '', kind: 3, tags: tags }); const event = await publish({ content: '', kind: 3, tags: tags });
// prefetch data
const user = await fetchUserData(follows);
// redirect to next step // redirect to next step
if (event && user.status === 'ok') { if (event) {
navigate('/auth/onboarding/step-2', { replace: true }); db.account.follows = follows;
await db.updateAccount('follows', JSON.stringify(follows));
await db.updateAccount('circles', JSON.stringify(follows));
setEnrich();
navigate(-1);
} else { } else {
setLoading(false); setLoading(false);
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
console.log('error: ', e); toast(e);
} }
}; };
@ -71,7 +78,7 @@ export function OnboardEnrichScreen() {
</div> </div>
<div className="mx-auto mb-8 w-full max-w-md px-3"> <div className="mx-auto mb-8 w-full max-w-md px-3">
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100"> <h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
{loading ? 'Loading...' : 'Enrich your network'} Enrich your network
</h1> </h1>
</div> </div>
<div className="flex w-full flex-nowrap items-center gap-4 overflow-x-auto px-4 scrollbar-none"> <div className="flex w-full flex-nowrap items-center gap-4 overflow-x-auto px-4 scrollbar-none">

View File

@ -4,17 +4,20 @@ import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
import { WidgetKinds } from '@stores/widgets'; import { WidgetKinds } from '@stores/widgets';
const data = [ const data = [
{ hashtag: '#bitcoin' }, { hashtag: '#bitcoin' },
{ hashtag: '#nostr' }, { hashtag: '#nostr' },
{ hashtag: '#nostrdesign' }, { hashtag: '#nostrdesign' },
{ hashtag: '#security' },
{ hashtag: '#zap' }, { hashtag: '#zap' },
{ hashtag: '#LFG' }, { hashtag: '#LFG' },
{ hashtag: '#zapchain' }, { hashtag: '#zapchain' },
{ hashtag: '#shitcoin' },
{ hashtag: '#plebchain' }, { hashtag: '#plebchain' },
{ hashtag: '#nodes' }, { hashtag: '#nodes' },
{ hashtag: '#hodl' }, { hashtag: '#hodl' },
@ -23,21 +26,26 @@ const data = [
{ hashtag: '#meme' }, { hashtag: '#meme' },
{ hashtag: '#memes' }, { hashtag: '#memes' },
{ hashtag: '#memestr' }, { hashtag: '#memestr' },
{ hashtag: '#penisbutter' }, { hashtag: '#nostriches' },
{ hashtag: '#dev' },
{ hashtag: '#anime' }, { hashtag: '#anime' },
{ hashtag: '#waifu' }, { hashtag: '#waifu' },
{ hashtag: '#manga' }, { hashtag: '#manga' },
{ hashtag: '#nostriches' }, { hashtag: '#lume' },
{ hashtag: '#dev' }, { hashtag: '#snort' },
{ hashtag: '#damus' },
{ hashtag: '#primal' },
]; ];
export function OnboardHashtagScreen() { export function OnboardHashtagScreen() {
const { db } = useStorage(); const { db } = useStorage();
const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [tags, setTags] = useState(new Set<string>()); const [tags, setTags] = useState(new Set<string>());
const navigate = useNavigate();
const setHashtag = useOnboarding((state) => state.toggleHashtag);
const toggleTag = (tag: string) => { const toggleTag = (tag: string) => {
if (tags.has(tag)) { if (tags.has(tag)) {
setTags((prev) => { setTags((prev) => {
@ -50,13 +58,6 @@ export function OnboardHashtagScreen() {
} }
}; };
const skip = async () => {
// update last login
await db.updateLastLogin();
navigate('/auth/complete', { replace: true });
};
const submit = async () => { const submit = async () => {
try { try {
setLoading(true); setLoading(true);
@ -65,10 +66,8 @@ export function OnboardHashtagScreen() {
await db.createWidget(WidgetKinds.global.hashtag, tag, tag.replace('#', '')); await db.createWidget(WidgetKinds.global.hashtag, tag, tag.replace('#', ''));
} }
// update last login setHashtag();
await db.updateLastLogin(); navigate(-1);
navigate('/auth/complete', { replace: true });
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
await message(e, { title: 'Lume', type: 'error' }); await message(e, { title: 'Lume', type: 'error' });
@ -76,64 +75,55 @@ export function OnboardHashtagScreen() {
}; };
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="relative flex h-full w-full flex-col justify-center">
<div className="mb-4 border-b border-white/10 pb-4"> <div className="absolute left-[8px] top-4">
<h1 className="mb-2 text-center text-2xl font-semibold text-white"> <button
Choose {tags.size}/3 your favorite hashtags onClick={() => navigate(-1)}
</h1> className="inline-flex items-center gap-2 text-sm font-medium"
<p className="text-white/70"> >
Hashtags are an easy way to discover more content. By adding a hashtag, Lume <div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
will show all related posts. You can always add more later. <ArrowLeftIcon className="h-5 w-5" />
</p> </div>
Back
</button>
</div> </div>
<div className="flex flex-col gap-4"> <div className="mx-auto flex w-full max-w-md flex-col gap-10 px-3">
<div className="flex h-[450px] w-full flex-col divide-y divide-white/5 overflow-y-auto rounded-xl bg-white/20 backdrop-blur-xl scrollbar-none"> <h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
{data.map((item: { hashtag: string }) => ( Choose {tags.size}/3 your favorite hashtag
<button </h1>
key={item.hashtag} <div className="flex flex-col gap-4">
type="button" <div className="flex h-[420px] w-full flex-col overflow-y-auto rounded-xl bg-neutral-100 dark:bg-neutral-900">
onClick={() => toggleTag(item.hashtag)} {data.map((item: { hashtag: string }) => (
className="inline-flex transform items-center justify-between px-4 py-2 hover:bg-white/10" <button
> key={item.hashtag}
<p className="text-white">{item.hashtag}</p> type="button"
{tags.has(item.hashtag) && ( onClick={() => toggleTag(item.hashtag)}
<div> className="inline-flex items-center justify-between px-4 py-2 hover:bg-neutral-300 dark:hover:bg-neutral-700"
<CheckCircleIcon className="h-4 w-4 text-green-400" /> >
</div> <p className="text-neutral-900 dark:text-neutral-100">{item.hashtag}</p>
)} {tags.has(item.hashtag) && (
</button> <div>
))} <CheckCircleIcon className="h-5 w-5 text-teal-500" />
</div> </div>
<div className="flex flex-col gap-2"> )}
</button>
))}
</div>
<button <button
type="button" type="button"
onClick={submit} onClick={submit}
disabled={loading || tags.size === 0 || tags.size > 3} disabled={loading || tags.size === 0}
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50" className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
> >
{loading ? ( {loading ? (
<> <>
<span className="w-5" /> <LoaderIcon className="h-4 w-4 animate-spin" />
<span>Creating...</span> <span>Adding...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : ( ) : (
<> <span>Add {tags.size} tags & Continue</span>
<span className="w-5" />
<span>Add {tags.size} tags & Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)} )}
</button> </button>
{!loading ? (
<button
type="button"
onClick={() => skip()}
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
>
Skip, you can add later
</button>
) : null}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,10 @@
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { CustomRelay } from '@app/auth/components/features/customRelay'; import { AllowNotification } from '@app/auth/components/features/allowNotification';
import { Circle } from '@app/auth/components/features/enableCircle';
import { OutboxModel } from '@app/auth/components/features/enableOutbox';
import { FavoriteHashtag } from '@app/auth/components/features/favoriteHashtag'; import { FavoriteHashtag } from '@app/auth/components/features/favoriteHashtag';
import { FollowList } from '@app/auth/components/features/followList'; import { FollowList } from '@app/auth/components/features/followList';
import { LinkList } from '@app/auth/components/features/linkList';
import { NIP04 } from '@app/auth/components/features/nip04';
import { SuggestFollow } from '@app/auth/components/features/suggestFollow'; import { SuggestFollow } from '@app/auth/components/features/suggestFollow';
export function OnboardingListScreen() { export function OnboardingListScreen() {
@ -25,9 +25,9 @@ export function OnboardingListScreen() {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{newuser ? <SuggestFollow /> : <FollowList />} {newuser ? <SuggestFollow /> : <FollowList />}
<FavoriteHashtag /> <FavoriteHashtag />
<LinkList /> <Circle />
<NIP04 /> <OutboxModel />
<CustomRelay /> <AllowNotification />
<Link <Link
to="/" to="/"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600" className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"

View File

@ -1,53 +1,37 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { FULL_RELAYS } from '@stores/constants'; import { useOnboarding } from '@stores/onboarding';
import { useNostr } from '@utils/hooks/useNostr'; import { useNostr } from '@utils/hooks/useNostr';
export function OnboardRelaysScreen() { export function OnboardRelaysScreen() {
const navigate = useNavigate(); const navigate = useNavigate();
const toggleRelays = useOnboarding((state) => state.toggleRelays);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [relays, setRelays] = useState(new Set<string>()); const [relays, setRelays] = useState(new Set<string>());
const { publish } = useNostr(); const { publish } = useNostr();
const { db } = useStorage(); const { db } = useStorage();
const { ndk } = useNDK(); const { getAllRelaysByUsers } = useNostr();
const { status, data } = useQuery( const { status, data } = useQuery(
['relays'], ['relays'],
async () => { async () => {
const tmp = new Map<string, string>(); return await getAllRelaysByUsers();
const events = await ndk.fetchEvents({
kinds: [10002],
authors: db.account.follows,
});
if (events) {
events.forEach((event) => {
event.tags.forEach((tag) => {
tmp.set(tag[1], event.pubkey);
});
});
}
return tmp;
}, },
{ {
enabled: db.account ? true : false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
} }
); );
const relaysAsArray = Array.from(data?.keys() || []);
const toggleRelay = (relay: string) => { const toggleRelay = (relay: string) => {
if (relays.has(relay)) { if (relays.has(relay)) {
setRelays((prev) => { setRelays((prev) => {
@ -59,120 +43,110 @@ export function OnboardRelaysScreen() {
} }
}; };
const submit = async (skip?: boolean) => { const submit = async () => {
try { try {
setLoading(true); setLoading(true);
if (!skip) { for (const relay of relays) {
for (const relay of relays) { await db.createRelay(relay);
await db.createRelay(relay);
}
const tags = Array.from(relays).map((relay) => ['r', relay.replace(/\/+$/, '')]);
await publish({ content: '', kind: 10002, tags: tags });
} else {
for (const relay of FULL_RELAYS) {
await db.createRelay(relay);
}
} }
// update last login const tags = Array.from(relays).map((relay) => ['r', relay.replace(/\/+$/, '')]);
await db.updateLastLogin(); await publish({ content: '', kind: 10002, tags: tags });
navigate('/', { replace: true }); toggleRelays();
navigate(-1);
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
console.log('error: ', e); toast.error(e);
} }
}; };
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="relative flex h-full w-full flex-col justify-center">
<div className="mb-8 text-center"> <div className="absolute left-[8px] top-4">
<h1 className="text-xl font-semibold text-white">Relay discovery</h1> <button
<p className="text-sm text-white/50"> onClick={() => navigate(-1)}
You can add relay which is using by who you&apos;re following to easier reach className="inline-flex items-center gap-2 text-sm font-medium"
their content. Learn more about relay{' '} >
<a <div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
href="https://nostr.com/relays" <ArrowLeftIcon className="h-5 w-5" />
target="_blank" </div>
rel="noreferrer" Back
className="text-blue-500 underline" </button>
>
here (nostr.com)
</a>
</p>
</div> </div>
<div className="flex flex-col gap-4"> <div className="mx-auto flex w-full max-w-md flex-col gap-10 px-3">
<div className="relative flex h-[500px] w-full flex-col divide-y divide-white/10 overflow-y-auto rounded-xl bg-white/10 backdrop-blur-xl scrollbar-none"> <h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
{status === 'loading' ? ( Relay discovery
<div className="flex h-full w-full items-center justify-center"> </h1>
<LoaderIcon className="h-4 w-4 animate-spin text-white" /> <div className="flex flex-col gap-4">
</div> <div className="flex h-[420px] w-full flex-col overflow-y-auto rounded-xl bg-neutral-100 dark:bg-neutral-900">
) : relaysAsArray.length === 0 ? ( {status === 'loading' ? (
<div className="flex h-full w-full items-center justify-center px-6"> <div className="flex h-full w-full items-center justify-center">
<p className="text-center text-white/50"> <LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
Lume couldn&apos;t find any relays from your follows. </div>
<br /> ) : data.size === 0 ? (
You can skip this step and use default relays instead. <div className="flex h-full w-full items-center justify-center px-6">
</p> <p className="text-center text-neutral-300 dark:text-neutral-600">
</div> Lume couldn&apos;t find any relays from your follows.
) : ( <br />
relaysAsArray.map((item, index) => ( You can skip this step and use default relays instead.
<button </p>
key={item + index} </div>
type="button" ) : (
onClick={() => toggleRelay(item)} [...data].map(([key, value]) => (
className="inline-flex transform items-start justify-between bg-white/10 px-4 py-2 backdrop-blur-xl hover:bg-white/20" <button
> key={key}
<div className="flex flex-col items-start gap-1"> type="button"
<p className="max-w-[15rem] truncate">{item.replace(/\/+$/, '')}</p> onClick={() => toggleRelay(key)}
<User pubkey={data.get(item)} variant="mention" /> className="inline-flex transform items-start justify-between px-4 py-2 hover:bg-neutral-300 dark:hover:bg-neutral-700"
</div> >
{relays.has(item) && ( <div className="flex w-full items-center justify-between">
<div className="pt-1.5"> <div className="inline-flex items-center gap-2">
<CheckCircleIcon className="h-4 w-4 text-green-400" /> <div className="pt-1.5">
{relays.has(key) ? (
<CheckCircleIcon className="h-4 w-4 text-teal-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-neutral-300 dark:text-neutral-700" />
)}
</div>
<p className="max-w-[15rem] truncate">{key.replace(/\/+$/, '')}</p>
</div>
<div className="inline-flex items-center gap-2">
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
Used by
</span>
<div className="isolate flex -space-x-2">
{value.slice(0, 3).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{value.length > 3 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
<span className="text-xs font-medium">+{value.length}</span>
</div>
) : null}
</div>
</div>
</div> </div>
)} </button>
</button> ))
)) )}
)} </div>
{relays.size > 5 && (
<div className="sticky bottom-0 left-0 inline-flex w-full items-center justify-center bg-white/10 px-4 py-2 backdrop-blur-2xl">
<p className="text-sm text-orange-400">
Using too much relay can cause high resource usage
</p>
</div>
)}
</div>
<div className="flex flex-col gap-2">
<button <button
type="button" type="button"
disabled={loading}
onClick={() => submit()} onClick={() => submit()}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50" disabled={loading}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
> >
{loading ? ( {loading ? (
<> <>
<span className="w-5" /> <LoaderIcon className="h-4 w-4 animate-spin" />
<span>Creating...</span> <span>Adding...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</> </>
) : ( ) : (
<> <span>Add {relays.size} relays & Continue</span>
<span className="w-5" />
<span>Add {relays.size} relays & Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)} )}
</button> </button>
<button
type="button"
onClick={() => submit(true)}
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/10 focus:outline-none"
>
Skip, use Lume default relays
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +0,0 @@
export function CommunitiesScreen() {
return (
<div>
<p>TODO</p>
</div>
);
}

View File

@ -54,10 +54,13 @@ export const NDKInstance = () => {
async function initNDK() { async function initNDK() {
const explicitRelayUrls = await getExplicitRelays(); const explicitRelayUrls = await getExplicitRelays();
const outboxSetting = await db.getSettingValue('outbox');
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'lume_ndkcache' }); const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'lume_ndkcache' });
const instance = new NDK({ const instance = new NDK({
explicitRelayUrls, explicitRelayUrls,
cacheAdapter: dexieAdapter, cacheAdapter: dexieAdapter,
outboxRelayUrls: ['wss://purplepag.es'],
enableOutboxModel: outboxSetting === '1',
}); });
try { try {

View File

@ -24,6 +24,7 @@ export class LumeStorage {
public async secureLoad(key?: string) { public async secureLoad(key?: string) {
const value: string = await invoke('secure_load', { key }); const value: string = await invoke('secure_load', { key });
if (!value) return null;
return value; return value;
} }
@ -45,8 +46,8 @@ export class LumeStorage {
if (typeof account.follows === 'string') if (typeof account.follows === 'string')
account.follows = JSON.parse(account.follows); account.follows = JSON.parse(account.follows);
if (typeof account.network === 'string') if (typeof account.circles === 'string')
account.network = JSON.parse(account.network); account.circles = JSON.parse(account.circles);
if (typeof account.last_login_at === 'string') if (typeof account.last_login_at === 'string')
account.last_login_at = parseInt(account.last_login_at); account.last_login_at = parseInt(account.last_login_at);
@ -71,8 +72,8 @@ export class LumeStorage {
]); ]);
} else { } else {
await this.db.execute( await this.db.execute(
'INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, is_active) VALUES ($1, $2, $3, $4);', 'INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);',
[npub, pubkey, 'privkey is stored in secure storage', 1] [npub, pubkey, 1]
); );
} }
@ -80,7 +81,7 @@ export class LumeStorage {
return account; return account;
} }
public async updateAccount(column: string, value: string | string[]) { public async updateAccount(column: string, value: string) {
const insert = await this.db.execute( const insert = await this.db.execute(
`UPDATE accounts SET ${column} = $1 WHERE id = $2;`, `UPDATE accounts SET ${column} = $1 WHERE id = $2;`,
[value, this.account.id] [value, this.account.id]
@ -298,12 +299,22 @@ export class LumeStorage {
return await this.db.execute(`DELETE FROM relays WHERE relay = "${relay}";`); return await this.db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
} }
public async removePrivkey() { public async createSetting(key: string, value: string) {
return await this.db.execute( return await this.db.execute(
`UPDATE accounts SET privkey = "privkey is stored in secure storage" WHERE id = "${this.account.id}";` 'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
); );
} }
public async getSettingValue(key: string) {
const results: { key: string; value: string }[] = await this.db.select(
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
[key]
);
if (results.length < 1) return null;
return results[0].value;
}
public async accountLogout() { public async accountLogout() {
// update current account status // update current account status
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [ await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [

View File

@ -16,7 +16,7 @@ root.render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<StorageProvider> <StorageProvider>
<NDKProvider> <NDKProvider>
<Toaster /> <Toaster position="top-center" />
<App /> <App />
</NDKProvider> </NDKProvider>
</StorageProvider> </StorageProvider>

View File

@ -127,7 +127,7 @@ export const User = memo(function User({
</p> </p>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
className="markdown-simple line-clamp-6" className="markdown-simple line-clamp-6 whitespace-pre-line break-all"
disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6']} disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6']}
unwrapDisallowed={true} unwrapDisallowed={true}
linkTarget={'_blank'} linkTarget={'_blank'}

View File

@ -30,6 +30,7 @@ export function EventLoader({ firstTime }: { firstTime: boolean }) {
setIsFetched(); setIsFetched();
// invalidate queries // invalidate queries
queryClient.invalidateQueries(['local-network-widget']); queryClient.invalidateQueries(['local-network-widget']);
await db.updateLastLogin();
} }
} }
@ -47,10 +48,10 @@ export function EventLoader({ firstTime }: { firstTime: boolean }) {
{firstTime ? ( {firstTime ? (
<div> <div>
<span className="text-4xl">👋</span> <span className="text-4xl">👋</span>
<h3 className="mt-2 font-semibold leading-tight text-neutral-100 dark:text-neutral-900"> <h3 className="mt-2 font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
Hello, this is the first time you&apos;re using Lume Hello, this is the first time you&apos;re using Lume
</h3> </h3>
<p className="text-sm text-neutral-500"> <p className="text-sm text-neutral-600 dark:text-neutral-500">
Lume is downloading all events since the last 24 hours. It will auto Lume is downloading all events since the last 24 hours. It will auto
refresh when it done, please be patient refresh when it done, please be patient
</p> </p>

40
src/stores/onboarding.ts Normal file
View File

@ -0,0 +1,40 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
interface OnboardingState {
enrich: boolean;
hashtag: boolean;
circle: boolean;
relays: boolean;
outbox: boolean;
notification: boolean;
toggleEnrich: () => void;
toggleHashtag: () => void;
toggleCircle: () => void;
toggleRelays: () => void;
toggleOutbox: () => void;
toggleNotification: () => void;
}
export const useOnboarding = create<OnboardingState>()(
persist(
(set) => ({
enrich: false,
hashtag: false,
circle: false,
relays: false,
outbox: false,
notification: false,
toggleEnrich: () => set((state) => ({ enrich: !state.enrich })),
toggleHashtag: () => set((state) => ({ hashtag: !state.hashtag })),
toggleCircle: () => set((state) => ({ circle: !state.circle })),
toggleRelays: () => set((state) => ({ relays: !state.relays })),
toggleOutbox: () => set((state) => ({ outbox: !state.outbox })),
toggleNotification: () => set((state) => ({ notification: !state.notification })),
}),
{
name: 'onboarding',
storage: createJSONStorage(() => sessionStorage),
}
)
);

View File

@ -4,13 +4,11 @@ import {
NDKKind, NDKKind,
NDKPrivateKeySigner, NDKPrivateKeySigner,
NDKSubscription, NDKSubscription,
NDKUser,
} from '@nostr-dev-kit/ndk'; } from '@nostr-dev-kit/ndk';
import { message, open } from '@tauri-apps/plugin-dialog'; import { message, open } from '@tauri-apps/plugin-dialog';
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import { NostrEventExt } from 'nostr-fetch'; import { NostrEventExt } from 'nostr-fetch';
import { nip19 } from 'nostr-tools';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@ -53,67 +51,6 @@ export function useNostr() {
console.log('current active sub: ', subManager.size); console.log('current active sub: ', subManager.size);
}; };
const fetchUserData = async (preFollows?: string[]) => {
try {
const follows = new Set<string>(preFollows || []);
const lruNetwork = new LRUCache<string, string, void>({ max: 300 });
// fetch user's relays
const relayEvents = await ndk.fetchEvents({
kinds: [NDKKind.RelayList],
authors: [db.account.pubkey],
});
if (relayEvents) {
const latestRelayEvent = [...relayEvents].sort(
(a, b) => b.created_at - a.created_at
)[0];
if (latestRelayEvent) {
for (const item of latestRelayEvent.tags) {
await db.createRelay(item[1], item[2]);
}
}
}
// fetch user's follows
if (!preFollows) {
const user = ndk.getUser({ hexpubkey: db.account.pubkey });
const list = await user.follows();
list.forEach((item: NDKUser) => {
follows.add(nip19.decode(item.npub).data as string);
});
}
// build user's network
const followEvents = await ndk.fetchEvents({
kinds: [NDKKind.Contacts],
authors: [...follows],
limit: 300,
});
followEvents.forEach((event: NDKEvent) => {
event.tags.forEach((tag) => {
if (tag[0] === 'p') lruNetwork.set(tag[1], tag[1]);
});
});
// get lru values
const network = [...lruNetwork.values()] as string[];
// update db
await db.updateAccount('follows', [...follows]);
await db.updateAccount('network', [...new Set([...follows, ...network])]);
// clear lru caches
lruNetwork.clear();
return { status: 'ok', message: 'User data fetched' };
} catch (e) {
return { status: 'failed', message: e };
}
};
const addContact = async (pubkey: string) => { const addContact = async (pubkey: string) => {
const list = new Set(db.account.follows); const list = new Set(db.account.follows);
list.add(pubkey); list.add(pubkey);
@ -270,7 +207,7 @@ export function useNostr() {
if (!customSince) { if (!customSince) {
if (dbEventsEmpty || db.account.last_login_at === 0) { if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.network.length > 500 ? nHoursAgo(12) : nHoursAgo(24); since = db.account.circles.length > 500 ? nHoursAgo(12) : nHoursAgo(24);
} else { } else {
since = db.account.last_login_at; since = db.account.last_login_at;
} }
@ -282,7 +219,7 @@ export function useNostr() {
relayUrls, relayUrls,
{ {
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article], kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: db.account.network, authors: db.account.circles,
}, },
{ since: since } { since: since }
)) as unknown as NDKEvent[]; )) as unknown as NDKEvent[];
@ -344,7 +281,9 @@ export function useNostr() {
kind: NDKKind | number; kind: NDKKind | number;
tags: string[][]; tags: string[][];
}): Promise<NDKEvent> => { }): Promise<NDKEvent> => {
const privkey: string = await db.secureLoad(); const privkey: string = await db.secureLoad(db.account.pubkey);
// #TODO: show prompt
if (!privkey) return;
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
const signer = new NDKPrivateKeySigner(privkey); const signer = new NDKPrivateKeySigner(privkey);
@ -362,7 +301,9 @@ export function useNostr() {
}; };
const createZap = async (event: NDKEvent, amount: number, message?: string) => { const createZap = async (event: NDKEvent, amount: number, message?: string) => {
const privkey: string = await db.secureLoad(); const privkey: string = await db.secureLoad(db.account.pubkey);
// #TODO: show prompt
if (!privkey) return;
if (!ndk.signer) { if (!ndk.signer) {
const signer = new NDKPrivateKeySigner(privkey); const signer = new NDKPrivateKeySigner(privkey);
@ -459,7 +400,6 @@ export function useNostr() {
return { return {
sub, sub,
fetchUserData,
addContact, addContact,
removeContact, removeContact,
getAllNIP04Chats, getAllNIP04Chats,

View File

@ -26,7 +26,7 @@ export interface Account extends NDKUserProfile {
npub: string; npub: string;
pubkey: string; pubkey: string;
follows: null | string[]; follows: null | string[];
network: null | string[]; circles: null | string[];
is_active: number; is_active: number;
last_login_at: number; last_login_at: number;
} }