wip: polish
This commit is contained in:
@@ -67,6 +67,7 @@ export function Circle() {
|
||||
lru.clear();
|
||||
|
||||
// done
|
||||
await db.createSetting('circles', '1');
|
||||
setCircle();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import { downloadDir } from '@tauri-apps/api/path';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { message, save } from '@tauri-apps/plugin-dialog';
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -42,18 +43,14 @@ export function CreateAccountScreen() {
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon('lume new account', 90, 50));
|
||||
|
||||
const onSubmit = async (data: {
|
||||
name: string;
|
||||
display_name: string;
|
||||
about: string;
|
||||
}) => {
|
||||
const onSubmit = async (data: { name: string; about: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const profile = {
|
||||
...data,
|
||||
name: data.name,
|
||||
display_name: data.display_name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
};
|
||||
|
||||
@@ -93,6 +90,10 @@ export function CreateAccountScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const copyNsec = async () => {
|
||||
await writeText(keys.nsec);
|
||||
};
|
||||
|
||||
const download = async () => {
|
||||
try {
|
||||
const downloadPath = await downloadDir();
|
||||
@@ -116,7 +117,7 @@ export function CreateAccountScreen() {
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="absolute left-[8px] top-4">
|
||||
<div className="absolute left-[8px] top-2">
|
||||
{!keys ? (
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
@@ -230,11 +231,24 @@ export function CreateAccountScreen() {
|
||||
<label htmlFor="nsec" className="text-sm font-semibold">
|
||||
Private key
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={keys.nsec}
|
||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
readOnly
|
||||
value={
|
||||
keys.nsec.substring(0, 10) + '**************************'
|
||||
}
|
||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyNsec}
|
||||
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="nsec" className="text-sm font-semibold">
|
||||
|
||||
@@ -21,7 +21,7 @@ export function WelcomeScreen() {
|
||||
to="/auth/import"
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-lg font-medium text-neutral-900 hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-900"
|
||||
>
|
||||
Log in
|
||||
Log in with key
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { VList } from 'virtua';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { VList, VListHandle } from 'virtua';
|
||||
|
||||
import { ToggleWidgetList } from '@app/space/components/toggle';
|
||||
import { WidgetList } from '@app/space/components/widgetList';
|
||||
@@ -31,6 +31,7 @@ import { Widget } from '@utils/types';
|
||||
|
||||
export function SpaceScreen() {
|
||||
const { db } = useStorage();
|
||||
const vlistRef = useRef<VListHandle>(null);
|
||||
|
||||
const [widgets, fetchWidgets] = useWidgets((state) => [
|
||||
state.widgets,
|
||||
@@ -88,6 +89,7 @@ export function SpaceScreen() {
|
||||
<VList
|
||||
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none"
|
||||
horizontal
|
||||
ref={vlistRef}
|
||||
>
|
||||
{!widgets ? (
|
||||
<div className="flex h-full w-[420px] flex-col items-center justify-center">
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import * as Avatar from '@radix-ui/react-avatar';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { HorizontalDotsIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useActivities } from '@stores/activities';
|
||||
|
||||
@@ -19,6 +20,9 @@ export function ActiveAccount() {
|
||||
const { sub } = useNostr();
|
||||
|
||||
const addActivity = useActivities((state) => state.addActivity);
|
||||
const svgURI =
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon(db.account.pubkey, 90, 50));
|
||||
|
||||
useEffect(() => {
|
||||
const filter: NDKFilter = {
|
||||
@@ -57,12 +61,24 @@ export function ActiveAccount() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 rounded-lg bg-neutral-100 p-1 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800">
|
||||
<Link to={`/users/${db.account.pubkey}`} className="relative inline-block">
|
||||
<Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={db.account.npub}
|
||||
className="aspect-square h-auto w-full rounded-md"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 block h-2 w-2 rounded-full bg-emerald-500 ring-2 ring-neutral-100 dark:ring-neutral-900" />
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={db.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: 'auto' }}
|
||||
className="aspect-square h-auto w-full rounded-md"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={db.account.pubkeypubkey}
|
||||
className="aspect-square h-auto w-full rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<span className="absolute bottom-0 right-0 block h-2 w-2 rounded-full bg-teal-500 ring-2 ring-neutral-100 dark:ring-neutral-900" />
|
||||
</Link>
|
||||
<div className="inline-flex items-center justify-center rounded-md">
|
||||
<HorizontalDotsIcon className="h-4 w-4" />
|
||||
|
||||
@@ -25,7 +25,7 @@ export function AvatarUploader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-100 px-1.5 py-1 text-sm font-medium text-blue-500 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:bg-blue-800"
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-100 px-1.5 py-1 text-sm font-medium text-blue-500 hover:border-blue-300 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:border-blue-800 dark:hover:bg-blue-800"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ComposerModal() {
|
||||
<Dialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-lg bg-neutral-300 text-black hover:bg-blue-600 hover:text-white dark:bg-neutral-700 dark:text-white dark:hover:bg-blue-600"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-lg bg-neutral-100 text-black hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-white dark:hover:bg-blue-500"
|
||||
>
|
||||
<ComposeIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Navigation() {
|
||||
>
|
||||
<HomeIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-sm text-black dark:text-white">Home</div>
|
||||
<div className="text-sm font-medium text-black dark:text-white">Home</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
@@ -47,7 +47,7 @@ export function Navigation() {
|
||||
>
|
||||
<ChatsIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-sm text-black dark:text-white">Chats</div>
|
||||
<div className="text-sm font-medium text-black dark:text-white">Chats</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
@@ -68,7 +68,7 @@ export function Navigation() {
|
||||
>
|
||||
<RelayIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-sm text-black dark:text-white">Relays</div>
|
||||
<div className="text-sm font-medium text-black dark:text-white">Relays</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
@@ -89,7 +89,9 @@ export function Navigation() {
|
||||
>
|
||||
<ExploreIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-sm text-black dark:text-white">Explore</div>
|
||||
<div className="text-sm font-medium text-black dark:text-white">
|
||||
Explore
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
@@ -98,7 +100,7 @@ export function Navigation() {
|
||||
<ComposerModal />
|
||||
<Link
|
||||
to="/nwc"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-lg bg-neutral-100 hover:bg-blue-600 hover:text-white dark:bg-neutral-900 dark:hover:bg-blue-600"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-lg bg-neutral-100 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:hover:bg-blue-500"
|
||||
>
|
||||
<NwcIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useWidgets } from '@stores/widgets';
|
||||
|
||||
export function TitleBar({ id, title }: { id?: string; title: string }) {
|
||||
export function TitleBar({ id, title }: { id?: string; title?: string }) {
|
||||
const { db } = useStorage();
|
||||
const remove = useWidgets((state) => state.removeWidget);
|
||||
|
||||
return (
|
||||
<div className="flex h-11 w-full shrink-0 items-center justify-between overflow-hidden px-3">
|
||||
<div className="w-6" />
|
||||
<h3 className="text-sm font-medium tracking-wide text-neutral-900 dark:text-neutral-100">
|
||||
{title}
|
||||
</h3>
|
||||
{id ? (
|
||||
{id === '9999' ? (
|
||||
<div className="isolate flex -space-x-2">
|
||||
{db.account.circles
|
||||
?.slice(0, 4)
|
||||
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
|
||||
{db.account.circles?.length > 4 ? (
|
||||
<div className="inline-flex h-6 w-6 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">
|
||||
+{db.account.circles?.length - 4}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-sm font-medium tracking-wide text-neutral-900 dark:text-neutral-100">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
{id !== '9999' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(db, id)}
|
||||
|
||||
@@ -33,7 +33,8 @@ export const User = memo(function User({
|
||||
| 'large'
|
||||
| 'thread'
|
||||
| 'avatar'
|
||||
| 'stacked';
|
||||
| 'stacked'
|
||||
| 'ministacked';
|
||||
embedProfile?: string;
|
||||
}) {
|
||||
const { status, user } = useProfile(pubkey, embedProfile);
|
||||
@@ -228,6 +229,28 @@ export const User = memo(function User({
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'ministacked') {
|
||||
return (
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: 'auto' }}
|
||||
className="inline-block h-6 w-6 rounded-full ring-1 ring-white dark:ring-black"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={pubkey}
|
||||
className="inline-block h-6 w-6 rounded-full bg-black ring-1 ring-white dark:bg-white dark:ring-black"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'repost') {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
|
||||
@@ -86,10 +86,10 @@ export function LocalNetworkWidget() {
|
||||
// subscribe for new event
|
||||
// sub will be managed by lru-cache
|
||||
useEffect(() => {
|
||||
if (db.account && db.account.network && dbEvents.length > 0) {
|
||||
if (db.account && db.account.circles && dbEvents.length > 0) {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: db.account.network,
|
||||
authors: db.account.circles,
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
@@ -102,7 +102,7 @@ export function LocalNetworkWidget() {
|
||||
|
||||
return (
|
||||
<WidgetWrapper>
|
||||
<TitleBar title="Network" />
|
||||
<TitleBar id="9999" />
|
||||
<div className="flex-1">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
|
||||
@@ -57,7 +57,7 @@ export function XfeedsWidget({ params }: { params: Widget }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-[500px] w-full flex-col overflow-y-auto rounded-lg bg-neutral-200 py-2 scrollbar-none dark:bg-neutral-800">
|
||||
{db.account.network.map((item: string) => (
|
||||
{db.account.circles.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
|
||||
@@ -47,7 +47,7 @@ export const WidgetKinds = {
|
||||
|
||||
export const DefaultWidgets: Array<WidgetGroup> = [
|
||||
{
|
||||
title: 'Network / Follows',
|
||||
title: 'Circles / Follows',
|
||||
data: [
|
||||
{
|
||||
kind: WidgetKinds.tmp.xfeed,
|
||||
@@ -57,12 +57,12 @@ export const DefaultWidgets: Array<WidgetGroup> = [
|
||||
{
|
||||
kind: WidgetKinds.local.files,
|
||||
title: 'Files',
|
||||
description: 'All files shared by people in your network',
|
||||
description: 'All files shared by people in your circle',
|
||||
},
|
||||
{
|
||||
kind: WidgetKinds.local.articles,
|
||||
title: 'Articles',
|
||||
description: 'All articles shared by people in your network',
|
||||
description: 'All articles shared by people in your circle',
|
||||
},
|
||||
{
|
||||
kind: WidgetKinds.local.follows,
|
||||
@@ -130,7 +130,7 @@ export const useWidgets = create<WidgetState>()(
|
||||
// default: add network widget
|
||||
dbWidgets.unshift({
|
||||
id: '9999',
|
||||
title: 'Network',
|
||||
title: '',
|
||||
content: '',
|
||||
kind: WidgetKinds.local.network,
|
||||
});
|
||||
|
||||
@@ -404,14 +404,14 @@ export function useNostr() {
|
||||
removeContact,
|
||||
getAllNIP04Chats,
|
||||
getAllEventsSinceLastLogin,
|
||||
getContactsByPubkey,
|
||||
getEventsByPubkey,
|
||||
getAllRelaysByUsers,
|
||||
fetchActivities,
|
||||
fetchNIP04Messages,
|
||||
fetchAllReplies,
|
||||
publish,
|
||||
createZap,
|
||||
upload,
|
||||
getContactsByPubkey,
|
||||
getEventsByPubkey,
|
||||
getAllRelaysByUsers,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user