mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
ok fine
This commit is contained in:
parent
489ab6bd0b
commit
939a72f945
@ -98,6 +98,13 @@ fn secure_load(key: String) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn secure_remove(key: String) -> Result<(), ()> {
|
||||||
|
let entry = Entry::new("lume", &key).expect("Failed to create entry");
|
||||||
|
let _ = entry.delete_password();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_app::init())
|
.plugin(tauri_plugin_app::init())
|
||||||
@ -140,7 +147,8 @@ fn main() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
opengraph,
|
opengraph,
|
||||||
secure_save,
|
secure_save,
|
||||||
secure_load
|
secure_load,
|
||||||
|
secure_remove
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -46,7 +46,7 @@ input::-ms-clear {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror p.is-empty::before {
|
.ProseMirror p.is-empty::before {
|
||||||
@apply text-white/50;
|
@apply text-neutral-600 dark:text-neutral-400;
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { message } from '@tauri-apps/plugin-dialog';
|
import { message } from '@tauri-apps/plugin-dialog';
|
||||||
import { fetch } from '@tauri-apps/plugin-http';
|
|
||||||
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export function FollowList() {
|
|||||||
<User key={item} pubkey={item} variant="stacked" />
|
<User key={item} pubkey={item} variant="stacked" />
|
||||||
))}
|
))}
|
||||||
{data.length > 16 ? (
|
{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">
|
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-200 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-800">
|
||||||
<span className="text-xs font-medium">+{data.length}</span>
|
<span className="text-xs font-medium">+{data.length}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -46,6 +47,11 @@ export function ImportAccountScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pasteNsec = async () => {
|
||||||
|
const tempNsec = await readText();
|
||||||
|
setNsec(tempNsec);
|
||||||
|
};
|
||||||
|
|
||||||
const submitNsec = async () => {
|
const submitNsec = async () => {
|
||||||
if (savedPrivkey) return;
|
if (savedPrivkey) return;
|
||||||
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
|
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
|
||||||
@ -61,7 +67,7 @@ export function ImportAccountScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
<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">
|
||||||
{!created ? (
|
{!created ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
@ -159,30 +165,45 @@ export function ImportAccountScreen() {
|
|||||||
Enter your private key (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
|
<div className="relative flex-1">
|
||||||
name="nsec"
|
<input
|
||||||
type="text"
|
name="nsec"
|
||||||
value={nsec}
|
type="text"
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
value={nsec}
|
||||||
spellCheck={false}
|
onChange={(e) => setNsec(e.target.value)}
|
||||||
autoComplete="off"
|
spellCheck={false}
|
||||||
autoCorrect="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCorrect="off"
|
||||||
placeholder="nsec1"
|
autoCapitalize="off"
|
||||||
className="h-11 flex-1 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
placeholder="nsec1"
|
||||||
/>
|
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"
|
||||||
<button
|
/>
|
||||||
type="button"
|
{nsec.length < 5 ? (
|
||||||
onClick={submitNsec}
|
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
||||||
className={twMerge(
|
<button
|
||||||
'h-11 w-24 shrink-0 rounded-lg font-semibold text-white',
|
type="button"
|
||||||
!savedPrivkey
|
onClick={pasteNsec}
|
||||||
? 'bg-blue-500 hover:bg-blue-600'
|
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"
|
||||||
: 'bg-teal-500 hover:bg-teal-600'
|
>
|
||||||
)}
|
Paste
|
||||||
>
|
</button>
|
||||||
{savedPrivkey ? 'Saved' : 'Save'}
|
</div>
|
||||||
</button>
|
) : null}
|
||||||
|
</div>
|
||||||
|
{nsec.length > 5 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={submitNsec}
|
||||||
|
className={twMerge(
|
||||||
|
'h-11 w-24 shrink-0 rounded-lg font-semibold text-white',
|
||||||
|
!savedPrivkey
|
||||||
|
? 'bg-blue-500 hover:bg-blue-600'
|
||||||
|
: 'bg-teal-500 hover:bg-teal-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{savedPrivkey ? 'Saved' : 'Save'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 select-text">
|
<div className="mt-3 select-text">
|
||||||
@ -195,12 +216,14 @@ export function ImportAccountScreen() {
|
|||||||
1. In case you store private key 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 private key to{' '}
|
||||||
{db.platform === 'macos'
|
<b>
|
||||||
? 'Apple Keychain (macOS)'
|
{db.platform === 'macos'
|
||||||
: db.platform === 'windows'
|
? 'Apple Keychain (macOS)'
|
||||||
? 'Credential Manager (Windows)'
|
: db.platform === 'windows'
|
||||||
: 'Secret Service (Linux)'}
|
? 'Credential Manager (Windows)'
|
||||||
|
: 'Secret Service (Linux)'}
|
||||||
|
</b>
|
||||||
, 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">
|
||||||
|
@ -29,12 +29,7 @@ export function ChatScreen() {
|
|||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(message: NDKEvent) => {
|
(message: NDKEvent) => {
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage message={message} self={message.pubkey === db.account.pubkey} />
|
||||||
message={message}
|
|
||||||
userPubkey={db.account.pubkey}
|
|
||||||
userPrivkey={''}
|
|
||||||
self={message.pubkey === db.account.pubkey}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[data]
|
[data]
|
||||||
@ -91,17 +86,12 @@ export function ChatScreen() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<VList
|
<VList ref={listRef} className="h-full scrollbar-none" shift={true} reverse>
|
||||||
ref={listRef}
|
|
||||||
className="h-full scrollbar-none"
|
|
||||||
mode="reverse"
|
|
||||||
shift={true}
|
|
||||||
>
|
|
||||||
{data.map((message) => renderItem(message))}
|
{data.map((message) => renderItem(message))}
|
||||||
</VList>
|
</VList>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 rounded-b-lg border-t border-neutral-300 bg-neutral-200 p-3 backdrop-blur-xl dark:border-neutral-700 dark:bg-neutral-800">
|
<div className="shrink-0 rounded-b-lg border-t border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
<ChatForm
|
<ChatForm
|
||||||
receiverPubkey={pubkey}
|
receiverPubkey={pubkey}
|
||||||
userPubkey={db.account.pubkey}
|
userPubkey={db.account.pubkey}
|
||||||
|
@ -57,12 +57,12 @@ export function ChatForm({
|
|||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
placeholder="Message"
|
placeholder="Message"
|
||||||
className="h-10 flex-1 resize-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-500 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-300"
|
className="h-10 flex-1 resize-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-600 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-300"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
className="inline-flex shrink-0 items-center gap-1.5 text-sm font-medium text-neutral-500 dark:text-neutral-300"
|
className="inline-flex shrink-0 items-center gap-1.5 text-sm font-medium text-neutral-600 dark:text-neutral-300"
|
||||||
>
|
>
|
||||||
<EnterIcon className="h-5 w-5" />
|
<EnterIcon className="h-5 w-5" />
|
||||||
Send
|
Send
|
||||||
|
@ -29,7 +29,7 @@ export function MediaUploader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => uploadMedia()}
|
onClick={() => uploadMedia()}
|
||||||
className="group inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-neutral-500 hover:bg-neutral-400 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600"
|
className="group inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-neutral-600 hover:bg-neutral-400 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
|
@ -7,18 +7,8 @@ import { ImagePreview, LinkPreview, MentionNote, VideoPreview } from '@shared/no
|
|||||||
|
|
||||||
import { parser } from '@utils/parser';
|
import { parser } from '@utils/parser';
|
||||||
|
|
||||||
export function ChatMessage({
|
export function ChatMessage({ message, self }: { message: NDKEvent; self: boolean }) {
|
||||||
message,
|
const decryptedContent = useDecryptMessage(message);
|
||||||
userPubkey,
|
|
||||||
userPrivkey,
|
|
||||||
self,
|
|
||||||
}: {
|
|
||||||
message: NDKEvent;
|
|
||||||
userPubkey: string;
|
|
||||||
userPrivkey: string;
|
|
||||||
self: boolean;
|
|
||||||
}) {
|
|
||||||
const decryptedContent = useDecryptMessage(message, userPubkey, userPrivkey);
|
|
||||||
const richContent = parser(decryptedContent) ?? null;
|
const richContent = parser(decryptedContent) ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,17 +16,15 @@ export function ChatMessage({
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
'my-2 w-max max-w-[400px] rounded-t-xl px-3 py-3',
|
'my-2 w-max max-w-[400px] rounded-t-xl px-3 py-3',
|
||||||
self
|
self
|
||||||
? 'ml-auto rounded-l-xl bg-blue-500'
|
? 'ml-auto rounded-l-xl bg-blue-500 text-white'
|
||||||
: 'rounded-r-xl bg-neutral-200 dark:bg-neutral-800'
|
: 'rounded-r-xl bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!richContent ? (
|
{!richContent ? (
|
||||||
<p className="text-neutral-900 dark:text-neutral-100">Decrypting...</p>
|
<p>Decrypting...</p>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<p className="select-text whitespace-pre-line text-neutral-900 dark:text-neutral-100">
|
<p className="select-text whitespace-pre-line">{richContent.parsed}</p>
|
||||||
{richContent.parsed}
|
|
||||||
</p>
|
|
||||||
<div>
|
<div>
|
||||||
{richContent.images.length > 0 && <ImagePreview urls={richContent.images} />}
|
{richContent.images.length > 0 && <ImagePreview urls={richContent.images} />}
|
||||||
{richContent.videos.length > 0 && <VideoPreview urls={richContent.videos} />}
|
{richContent.videos.length > 0 && <VideoPreview urls={richContent.videos} />}
|
||||||
|
@ -11,7 +11,7 @@ export function useDecryptMessage(message: NDKEvent) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function decryptContent() {
|
async function decryptContent() {
|
||||||
try {
|
try {
|
||||||
const privkey = await db.secureLoad();
|
const privkey = await db.secureLoad(db.account.pubkey);
|
||||||
const sender =
|
const sender =
|
||||||
db.account.pubkey === message.pubkey
|
db.account.pubkey === message.pubkey
|
||||||
? message.tags.find((el) => el[0] === 'p')[1]
|
? message.tags.find((el) => el[0] === 'p')[1]
|
||||||
|
@ -36,9 +36,9 @@ export function ChatsScreen() {
|
|||||||
<div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800">
|
<div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-11 w-full shrink-0 items-center border-b border-white/5 px-3"
|
className="flex h-11 w-full shrink-0 items-center border-b border-neutral-200 px-3 dark:border-neutral-800"
|
||||||
>
|
>
|
||||||
<h3 className="bg-gradient-to-r from-blue-400 via-red-400 to-orange-500 bg-clip-text font-semibold text-transparent dark:from-blue-200 dark:via-red-200 dark:to-orange-300">
|
<h3 className="font-semibold text-neutral-950 dark:text-neutral-50">
|
||||||
All chats
|
All chats
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,19 +55,21 @@ export function UserLatestPosts({ pubkey }: { pubkey: string }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 border-t border-white/5 pt-3">
|
<div className="mt-4 border-t border-neutral-300 pt-3 dark:border-neutral-700">
|
||||||
<h3 className="mb-4 px-3 font-semibold text-white">Latest post</h3>
|
<h3 className="mb-4 px-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
Latest post
|
||||||
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<div className="inline-flex h-16 w-full items-center justify-center gap-1.5 rounded-lg bg-white/10 text-sm font-medium text-white/70">
|
<div className="inline-flex h-16 w-full items-center justify-center gap-1.5 rounded-lg bg-neutral-300 text-sm font-medium dark:bg-neutral-700">
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
Loading latest posts...
|
Loading latest posts...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : data.length < 1 ? (
|
) : data.length < 1 ? (
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<div className="inline-flex h-16 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white/70">
|
<div className="inline-flex h-16 w-full items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium dark:bg-neutral-700">
|
||||||
No posts from 24 hours ago
|
No posts from 24 hours ago
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,6 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { Image } from '@shared/image';
|
|
||||||
import { NIP05 } from '@shared/nip05';
|
import { NIP05 } from '@shared/nip05';
|
||||||
import { TextNote } from '@shared/notes';
|
import { TextNote } from '@shared/notes';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
@ -61,7 +60,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
|
|||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] animate-slideRightAndFade items-center justify-center px-4 pb-4 pt-16 transition-all">
|
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] animate-slideRightAndFade items-center justify-center px-4 pb-4 pt-16 transition-all">
|
||||||
<div className="h-full w-full overflow-y-auto rounded-lg border-t border-white/10 bg-white/20 py-3 backdrop-blur-3xl">
|
<div className="h-full w-full overflow-y-auto rounded-lg border border-neutral-300 bg-neutral-200 py-3 dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div>
|
<div>
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
@ -69,35 +68,40 @@ export const UserWithDrawer = memo(function UserWithDrawer({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-3 px-3">
|
<div className="flex flex-col gap-3 px-3">
|
||||||
<Image
|
<img
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
className="h-12 w-12 rounded-lg"
|
className="h-12 w-12 rounded-lg"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-2">
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h5 className="text-lg font-semibold leading-none">
|
<div>
|
||||||
{user?.displayName || user?.name || 'No name'}
|
<h5 className="text-lg font-semibold">
|
||||||
</h5>
|
{user?.name || user?.display_name || user?.displayName}
|
||||||
{user?.nip05 ? (
|
</h5>
|
||||||
<NIP05
|
{user?.nip05 ? (
|
||||||
pubkey={pubkey}
|
<NIP05
|
||||||
nip05={user?.nip05}
|
pubkey={pubkey}
|
||||||
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
|
nip05={user?.nip05}
|
||||||
/>
|
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||||
) : (
|
/>
|
||||||
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
|
) : (
|
||||||
{displayNpub(pubkey, 16)}
|
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
</span>
|
{displayNpub(pubkey, 16)}
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{user?.about ? <TextNote content={user?.about} /> : null}
|
{user?.about ? <TextNote content={user?.about} /> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
{followed ? (
|
{followed ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => unfollowUser(pubkey)}
|
onClick={() => unfollowUser(pubkey)}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-blue-600"
|
className="inline-flex h-9 w-36 items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Unfollow
|
Unfollow
|
||||||
</button>
|
</button>
|
||||||
@ -105,14 +109,14 @@ export const UserWithDrawer = memo(function UserWithDrawer({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => followUser(pubkey)}
|
onClick={() => followUser(pubkey)}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-blue-600"
|
className="inline-flex h-9 w-36 items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Follow
|
Follow
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/chats/${pubkey}`}
|
to={`/chats/${pubkey}`}
|
||||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-blue-600"
|
className="inline-flex h-9 w-36 items-center justify-center rounded-lg bg-neutral-300 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { AddressPointer, EventPointer } from 'nostr-tools/lib/nip19';
|
import { AddressPointer, EventPointer } from 'nostr-tools/lib/types/nip19';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ export function ArticleNoteScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-none h-full w-full overflow-y-auto scroll-smooth">
|
<div className="h-full w-full overflow-y-auto scroll-smooth scrollbar-none">
|
||||||
<div className="container mx-auto px-4 pt-16 sm:px-6 lg:px-8">
|
<div className="container mx-auto px-4 pt-16 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-5">
|
<div className="grid grid-cols-5">
|
||||||
<div className="col-span-1 pr-8">
|
<div className="col-span-1 pr-8">
|
||||||
@ -66,20 +66,20 @@ export function ArticleNoteScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
className="inline-flex h-12 w-12 items-center justify-center rounded-xl border border-white/10 bg-white/5"
|
className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-5 w-5 text-white" />
|
<ArrowLeftIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex flex-col divide-y divide-white/5 rounded-xl border border-white/10 bg-white/5">
|
<div className="flex flex-col divide-y divide-neutral-300 rounded-xl bg-neutral-200 dark:divide-neutral-700 dark:bg-neutral-800">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={share}
|
onClick={share}
|
||||||
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-t-xl "
|
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-t-xl"
|
||||||
>
|
>
|
||||||
{isCopy ? (
|
{isCopy ? (
|
||||||
<CheckCircleIcon className="h-5 w-5 text-green-500" />
|
<CheckCircleIcon className="h-5 w-5 text-teal-500" />
|
||||||
) : (
|
) : (
|
||||||
<ShareIcon className="h-5 w-5 text-white" />
|
<ShareIcon className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -87,7 +87,7 @@ export function ArticleNoteScreen() {
|
|||||||
onClick={scrollToReply}
|
onClick={scrollToReply}
|
||||||
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
|
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
|
||||||
>
|
>
|
||||||
<ReplyIcon className="h-5 w-5 text-white" />
|
<ReplyIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,19 +101,19 @@ export function ArticleNoteScreen() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="h-min w-full px-3">
|
<div className="flex h-min w-full flex-col gap-1 px-3">
|
||||||
<div className="rounded-xl border-t border-white/10 bg-white/20 px-3 pt-3">
|
<div>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
||||||
<div className="mt-2">{renderKind(data)}</div>
|
<div className="mt-2">{renderKind(data)}</div>
|
||||||
<div>
|
<div className="mb-3 mt-3">
|
||||||
<NoteActions
|
<NoteActions
|
||||||
id={data.id}
|
id={data.id}
|
||||||
pubkey={data.pubkey}
|
pubkey={data.pubkey}
|
||||||
extraButtons={false}
|
extraButtons={false}
|
||||||
/>
|
/>
|
||||||
<NoteStats id={data.id} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NoteStats id={data.id} />
|
||||||
</div>
|
</div>
|
||||||
<div ref={replyRef} className="px-3">
|
<div ref={replyRef} className="px-3">
|
||||||
<NoteReplyForm id={data.id} pubkey={db.account.pubkey} />
|
<NoteReplyForm id={data.id} pubkey={db.account.pubkey} />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { EventPointer } from 'nostr-tools/lib/nip19';
|
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -18,7 +18,6 @@ import {
|
|||||||
UnknownNote,
|
UnknownNote,
|
||||||
} from '@shared/notes';
|
} from '@shared/notes';
|
||||||
import { RepliesList } from '@shared/notes/replies/list';
|
import { RepliesList } from '@shared/notes/replies/list';
|
||||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
@ -28,6 +27,9 @@ export function TextNoteScreen() {
|
|||||||
const replyRef = useRef(null);
|
const replyRef = useRef(null);
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { status, data } = useEvent(id);
|
const { status, data } = useEvent(id);
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ export function TextNoteScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-none h-full w-full overflow-y-auto scroll-smooth">
|
<div className="h-full w-full overflow-y-auto scroll-smooth scrollbar-none">
|
||||||
<div className="container mx-auto px-4 pt-16 sm:px-6 lg:px-8">
|
<div className="container mx-auto px-4 pt-16 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-5">
|
<div className="grid grid-cols-5">
|
||||||
<div className="col-span-1 pr-8">
|
<div className="col-span-1 pr-8">
|
||||||
@ -70,20 +72,20 @@ export function TextNoteScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
className="inline-flex h-12 w-12 items-center justify-center rounded-xl border border-white/10 bg-white/5"
|
className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-5 w-5 text-white" />
|
<ArrowLeftIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex flex-col divide-y divide-white/5 rounded-xl border border-white/10 bg-white/5">
|
<div className="flex flex-col divide-y divide-neutral-300 rounded-xl bg-neutral-200 dark:divide-neutral-700 dark:bg-neutral-800">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={share}
|
onClick={share}
|
||||||
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-t-xl "
|
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-t-xl"
|
||||||
>
|
>
|
||||||
{isCopy ? (
|
{isCopy ? (
|
||||||
<CheckCircleIcon className="h-5 w-5 text-green-500" />
|
<CheckCircleIcon className="h-5 w-5 text-teal-500" />
|
||||||
) : (
|
) : (
|
||||||
<ShareIcon className="h-5 w-5 text-white" />
|
<ShareIcon className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@ -91,7 +93,7 @@ export function TextNoteScreen() {
|
|||||||
onClick={scrollToReply}
|
onClick={scrollToReply}
|
||||||
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
|
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
|
||||||
>
|
>
|
||||||
<ReplyIcon className="h-5 w-5 text-white" />
|
<ReplyIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,20 +101,20 @@ export function TextNoteScreen() {
|
|||||||
<div className="col-span-3 flex flex-col gap-1.5">
|
<div className="col-span-3 flex flex-col gap-1.5">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
<div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||||
<NoteSkeleton />
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-min w-full px-3">
|
<div className="flex h-min w-full flex-col px-3">
|
||||||
<div className="rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
|
<div className="rounded-xl bg-neutral-100 px-3 pt-3 dark:bg-neutral-900">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
||||||
<div className="mt-2">{renderKind(data)}</div>
|
<div className="mt-2">{renderKind(data)}</div>
|
||||||
<div>
|
<div className="mb-3">
|
||||||
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
|
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
|
||||||
<NoteStats id={id} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NoteStats id={id} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={replyRef} className="px-3">
|
<div ref={replyRef} className="px-3">
|
||||||
|
@ -56,24 +56,26 @@ export function NWCAlby() {
|
|||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="inline-flex items-center gap-2.5">
|
<div className="inline-flex items-center gap-2.5">
|
||||||
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-orange-200">
|
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-neutral-200">
|
||||||
<AlbyIcon className="h-8 w-8" />
|
<AlbyIcon className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="font-semibold leading-tight text-white">Alby</h5>
|
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">Alby</h5>
|
||||||
<p className="text-sm leading-tight text-white/50">Require alby account</p>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Require alby account
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-9 w-min items-center justify-center rounded-md border-t border-white/10 bg-white/20 px-3 text-sm font-medium text-white hover:bg-green-500"
|
className="inline-flex h-9 w-min items-center justify-center rounded-md bg-neutral-300 px-3 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Portal className="relative z-10">
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
|
@ -71,12 +71,14 @@ export function NWCOther() {
|
|||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<div className="flex items-center justify-between pt-4">
|
<div className="flex items-center justify-between pt-4">
|
||||||
<div className="inline-flex items-center gap-2.5">
|
<div className="inline-flex items-center gap-2.5">
|
||||||
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-white/10">
|
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-700">
|
||||||
<WorldIcon className="h-5 w-5" />
|
<WorldIcon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="font-semibold leading-tight text-white">URI String</h5>
|
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
<p className="text-sm leading-tight text-white/50">
|
URI String
|
||||||
|
</h5>
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Using format nostr+walletconnect:
|
Using format nostr+walletconnect:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -84,14 +86,14 @@ export function NWCOther() {
|
|||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-9 w-min items-center justify-center rounded-md border-t border-white/10 bg-white/20 px-3 text-sm font-medium text-white hover:bg-green-500"
|
className="inline-flex h-9 w-min items-center justify-center rounded-md bg-neutral-300 px-3 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-white dark:bg-black" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||||
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
|
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
|
||||||
|
@ -1,36 +1,50 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { NWCAlby } from '@app/nwc/components/alby';
|
import { NWCAlby } from '@app/nwc/components/alby';
|
||||||
import { NWCOther } from '@app/nwc/components/other';
|
import { NWCOther } from '@app/nwc/components/other';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
import { CheckCircleIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function NWCScreen() {
|
export function NWCScreen() {
|
||||||
const walletConnectURL = 'test';
|
const { db } = useStorage();
|
||||||
|
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
||||||
|
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
// setWalletConnectURL('');
|
await db.secureRemove('nwc');
|
||||||
// await db.secureSave('walletConnectURL', '', 'nwc');
|
setWalletConnectURL(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getNWC() {
|
||||||
|
const nwc = await db.secureLoad('nwc');
|
||||||
|
if (nwc) setWalletConnectURL(nwc);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNWC();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-white/5">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="flex w-full flex-col gap-5">
|
<div className="flex w-full flex-col gap-5">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-2xl font-bold leading-tight">
|
<h3 className="text-2xl font-bold leading-tight">
|
||||||
Nostr Wallet Connect (Beta)
|
Nostr Wallet Connect (Beta)
|
||||||
</h3>
|
</h3>
|
||||||
<p className="leading-tight text-white/70">
|
<p className="leading-tight text-neutral-600 dark:text-neutral-400">
|
||||||
Sending tips easily via Bitcoin Lightning.
|
Sending tips easily via Bitcoin Lightning.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto max-w-lg">
|
<div className="mx-auto max-w-lg">
|
||||||
{!walletConnectURL ? (
|
{!walletConnectURL ? (
|
||||||
<div className="flex w-full flex-col gap-4 divide-y divide-white/5 rounded-xl border-t border-white/10 bg-white/20 p-3">
|
<div className="flex w-full flex-col gap-4 divide-y divide-neutral-200 rounded-xl bg-neutral-100 p-3 dark:divide-neutral-800 dark:bg-neutral-900">
|
||||||
<NWCAlby />
|
<NWCAlby />
|
||||||
<NWCOther />
|
<NWCOther />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col rounded-xl border-t border-white/10 bg-white/20 p-3">
|
<div className="flex w-full flex-col rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<div className="mb-1 inline-flex items-center gap-1.5 text-sm text-green-500">
|
<div className="mb-1 inline-flex items-center gap-1.5 text-sm text-teal-500">
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
<CheckCircleIcon className="h-4 w-4" />
|
||||||
<p>You're using nostr wallet connect</p>
|
<p>You're using nostr wallet connect</p>
|
||||||
</div>
|
</div>
|
||||||
@ -38,12 +52,12 @@ export function NWCScreen() {
|
|||||||
<textarea
|
<textarea
|
||||||
readOnly
|
readOnly
|
||||||
value={walletConnectURL.substring(0, 120) + '****'}
|
value={walletConnectURL.substring(0, 120) + '****'}
|
||||||
className="relative h-40 w-full resize-none rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
|
className="relative h-40 w-full resize-none rounded-lg bg-neutral-200 px-3 py-1 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove()}
|
onClick={() => remove()}
|
||||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-red-500 hover:bg-white/20 focus:outline-none disabled:opacity-50"
|
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||||
>
|
>
|
||||||
Remove connection
|
Remove connection
|
||||||
</button>
|
</button>
|
||||||
@ -52,17 +66,19 @@ export function NWCScreen() {
|
|||||||
)}
|
)}
|
||||||
<div className="mt-5 flex flex-col gap-4">
|
<div className="mt-5 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h5 className="text-sm font-bold text-white">Introduction</h5>
|
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
<p className="text-sm text-white/70">
|
Introduction
|
||||||
|
</h5>
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Nostr Wallet Connect (NWC) is a way for applications like Nostr clients to
|
Nostr Wallet Connect (NWC) is a way for applications like Nostr clients to
|
||||||
access a remote Lightning wallet through a standardized protocol.
|
access a remote Lightning wallet through a standardized protocol.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-white/70">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
To learn more about the details have a look at{' '}
|
To learn more about the details have a look at{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/getAlby/nips/blob/7-wallet-connect-patch/47.md"
|
href="https://github.com/getAlby/nips/blob/7-wallet-connect-patch/47.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-300"
|
className="text-blue-500"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
the specs (NIP47)
|
the specs (NIP47)
|
||||||
@ -70,38 +86,42 @@ export function NWCScreen() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h5 className="text-sm font-bold text-white">About tipping</h5>
|
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
<p className="text-sm text-white/70">
|
About tipping
|
||||||
|
</h5>
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Also known as Zap in other Nostr client.
|
Also known as Zap in other Nostr client.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-white/70">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Lume doesn't take any commission or platform fees when you tip
|
Lume doesn't take any commission or platform fees when you tip
|
||||||
someone.
|
someone.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-white/70">Lume doesn't hold your Bitcoin</p>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Lume doesn't hold your Bitcoin
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h5 className="text-sm font-bold text-white">
|
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
Recommend wallet that support NWC
|
Recommend wallet that support NWC
|
||||||
</h5>
|
</h5>
|
||||||
<p className="text-sm text-white/70">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Mutiny Wallet:{' '}
|
Mutiny Wallet:{' '}
|
||||||
<a
|
<a
|
||||||
href="https://www.mutinywallet.com/"
|
href="https://www.mutinywallet.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-300"
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
website
|
website
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-white/70">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Self hosted NWC on Umbrel :{' '}
|
Self hosted NWC on Umbrel :{' '}
|
||||||
<a
|
<a
|
||||||
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
|
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-300"
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
website
|
website
|
||||||
</a>
|
</a>
|
||||||
|
@ -53,9 +53,9 @@ export function RelayList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<VList className="mt-20 h-full scrollbar-none">
|
<VList className="h-full scrollbar-none">
|
||||||
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
<h3 className="bg-gradient-to-r from-blue-200 via-red-200 to-orange-300 bg-clip-text font-semibold text-transparent">
|
<h3 className="font-semibold text-neutral-950 dark:text-neutral-50">
|
||||||
All relays used by your follows
|
All relays used by your follows
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@ export function UserRelay() {
|
|||||||
{relayUrls.includes(item) ? (
|
{relayUrls.includes(item) ? (
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500"></span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2">
|
||||||
|
@ -25,23 +25,27 @@ export function RelayScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-cols-3">
|
<div className="grid h-full w-full grid-cols-3">
|
||||||
<div className="col-span-2 border-r border-white/5">
|
<div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900">
|
||||||
<div className="inline-flex h-16 w-full items-center gap-2.5 border-b border-white/5 px-3">
|
<div className="inline-flex h-16 w-full items-center gap-2.5 border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
<button type="button" onClick={() => navigate(-1)}>
|
<button type="button" onClick={() => navigate(-1)}>
|
||||||
<ArrowLeftIcon className="h-5 w-5 text-white/70 hover:text-white" />
|
<ArrowLeftIcon className="h-5 w-5 text-neutral-500 hover:text-neutral-600 dark:text-neutral-600 dark:hover:text-neutral-500" />
|
||||||
</button>
|
</button>
|
||||||
<h3 className="font-semibold text-white">Global events</h3>
|
<h3 className="font-semibold text-neutral-950 dark:text-neutral-50">
|
||||||
|
Global events
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<RelayEventList relayUrl={url} />
|
<RelayEventList relayUrl={url} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<div className="inline-flex h-16 w-full items-center border-b border-white/5 px-3">
|
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
<h3 className="font-semibold text-white">Information</h3>
|
<h3 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
Information
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 px-3">
|
<div className="mt-4 px-3">
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-white">
|
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
@ -58,24 +62,28 @@ export function RelayScreen() {
|
|||||||
{(resolvedRelay) => (
|
{(resolvedRelay) => (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold leading-tight text-white">
|
<h3 className="font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
|
||||||
{resolvedRelay.name}
|
{resolvedRelay.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-medium text-white/70">
|
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-500">
|
||||||
{resolvedRelay.description}
|
{resolvedRelay.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{resolvedRelay.pubkey ? (
|
{resolvedRelay.pubkey ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h5 className="text-sm font-semibold text-white/70">Owner:</h5>
|
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
<div className="w-full rounded-lg bg-white/10 px-2 py-2">
|
Owner:
|
||||||
|
</h5>
|
||||||
|
<div className="w-full rounded-lg bg-neutral-100 px-2 py-2 dark:bg-neutral-900">
|
||||||
<User pubkey={resolvedRelay.pubkey} variant="simple" />
|
<User pubkey={resolvedRelay.pubkey} variant="simple" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{resolvedRelay.contact ? (
|
{resolvedRelay.contact ? (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-semibold text-white/70">Contact:</h5>
|
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
|
Contact:
|
||||||
|
</h5>
|
||||||
<a
|
<a
|
||||||
href={`mailto:${resolvedRelay.contact}`}
|
href={`mailto:${resolvedRelay.contact}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -87,7 +95,9 @@ export function RelayScreen() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-semibold text-white/70">Software:</h5>
|
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
|
Software:
|
||||||
|
</h5>
|
||||||
<a
|
<a
|
||||||
href={resolvedRelay.software}
|
href={resolvedRelay.software}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -100,7 +110,7 @@ export function RelayScreen() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-semibold text-white/70">
|
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
Supported NIPs:
|
Supported NIPs:
|
||||||
</h5>
|
</h5>
|
||||||
<div className="mt-2 grid grid-cols-7 gap-2">
|
<div className="mt-2 grid grid-cols-7 gap-2">
|
||||||
@ -110,7 +120,7 @@ export function RelayScreen() {
|
|||||||
href={`https://nips.be/${item}`}
|
href={`https://nips.be/${item}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="inline-flex aspect-square h-auto w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium hover:bg-blue-600"
|
className="inline-flex aspect-square h-auto w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
</a>
|
</a>
|
||||||
@ -119,7 +129,9 @@ export function RelayScreen() {
|
|||||||
</div>
|
</div>
|
||||||
{resolvedRelay.limitation ? (
|
{resolvedRelay.limitation ? (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-semibold text-white/70">Limitation</h5>
|
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
|
Limitation
|
||||||
|
</h5>
|
||||||
<div className="flex flex-col gap-2 divide-y divide-white/5">
|
<div className="flex flex-col gap-2 divide-y divide-white/5">
|
||||||
{Object.keys(resolvedRelay.limitation).map((key, index) => {
|
{Object.keys(resolvedRelay.limitation).map((key, index) => {
|
||||||
return (
|
return (
|
||||||
@ -127,10 +139,10 @@ export function RelayScreen() {
|
|||||||
key={index}
|
key={index}
|
||||||
className="flex items-baseline justify-between pt-2"
|
className="flex items-baseline justify-between pt-2"
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium text-white">
|
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||||
{titleCase(key)}:
|
{titleCase(key)}:
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-medium text-white/70">
|
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||||
{resolvedRelay.limitation[key].toString()}
|
{resolvedRelay.limitation[key].toString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -149,7 +161,7 @@ export function RelayScreen() {
|
|||||||
>
|
>
|
||||||
Open payment website
|
Open payment website
|
||||||
</a>
|
</a>
|
||||||
<span className="text-center text-xs text-white/70">
|
<span className="text-center text-xs text-neutral-600 dark:text-neutral-400">
|
||||||
You need to make a payment to connect this relay
|
You need to make a payment to connect this relay
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,12 +22,16 @@ export class LumeStorage {
|
|||||||
return await invoke('secure_save', { key, value });
|
return await invoke('secure_save', { key, value });
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
if (!value) return null;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async secureRemove(key: string) {
|
||||||
|
return await invoke('secure_remove', { key });
|
||||||
|
}
|
||||||
|
|
||||||
public async checkAccount() {
|
public async checkAccount() {
|
||||||
const result: Array<{ total: string }> = await this.db.select(
|
const result: Array<{ total: string }> = await this.db.select(
|
||||||
'SELECT COUNT(*) AS "total" FROM accounts;'
|
'SELECT COUNT(*) AS "total" FROM accounts;'
|
||||||
|
@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { HorizontalDotsIcon } from '@shared/icons';
|
import { AccountMoreActions } from '@shared/accounts/more';
|
||||||
|
|
||||||
import { useActivities } from '@stores/activities';
|
import { useActivities } from '@stores/activities';
|
||||||
|
|
||||||
@ -80,9 +80,7 @@ export function ActiveAccount() {
|
|||||||
</Avatar.Root>
|
</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" />
|
<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>
|
</Link>
|
||||||
<div className="inline-flex items-center justify-center rounded-md">
|
<AccountMoreActions pubkey={db.account.pubkey} />
|
||||||
<HorizontalDotsIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,17 @@ export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
|||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group ml-auto inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-white/10"
|
className="inline-flex items-center justify-center rounded-md"
|
||||||
>
|
>
|
||||||
<HorizontalDotsIcon className="h-5 w-5 text-white/80 group-hover:text-white" />
|
<HorizontalDotsIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-white/10 bg-white/20 p-2 backdrop-blur-3xl focus:outline-none">
|
<DropdownMenu.Content className="ml-2 flex w-[200px] flex-col overflow-hidden rounded-xl bg-blue-500 p-2 focus:outline-none">
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-white/10 focus:outline-none"
|
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</Link>
|
</Link>
|
||||||
@ -31,7 +31,7 @@ export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
|||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/settings/backup`}
|
to={`/settings/backup`}
|
||||||
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-white/10 focus:outline-none"
|
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
Backup
|
Backup
|
||||||
</Link>
|
</Link>
|
||||||
@ -39,7 +39,7 @@ export function AccountMoreActions({ pubkey }: { pubkey: string }) {
|
|||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/settings/`}
|
to={`/settings/`}
|
||||||
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-white/10 focus:outline-none"
|
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -41,7 +41,8 @@ export function Composer() {
|
|||||||
content: JSON.parse(localStorage.getItem('editor-content') || '{}'),
|
content: JSON.parse(localStorage.getItem('editor-content') || '{}'),
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: 'h-full markdown break-all overflow-y-auto outline-none pr-2',
|
class:
|
||||||
|
'h-full prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500 break-all overflow-y-auto outline-none pr-2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
@ -114,7 +115,7 @@ export function Composer() {
|
|||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex h-full w-full gap-3 px-4 pb-4">
|
<div className="flex h-full w-full gap-3 px-4 pb-4">
|
||||||
<div className="flex w-10 shrink-0 items-center justify-center">
|
<div className="flex w-10 shrink-0 items-center justify-center">
|
||||||
<div className="h-full w-[2px] bg-white/10 backdrop-blur-xl" />
|
<div className="h-full w-[2px] bg-neutral-100 dark:bg-neutral-900" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<EditorContent
|
<EditorContent
|
||||||
@ -134,15 +135,15 @@ export function Composer() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => clearReply()}
|
onClick={() => clearReply()}
|
||||||
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-white/10 px-2 backdrop-blur-xl"
|
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-300 px-2 dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
<CancelIcon className="h-4 w-4 text-white" />
|
<CancelIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between rounded-b-xl border-t border-white/10 bg-white/5 p-2">
|
<div className="flex items-center justify-between rounded-b-xl border-t border-neutral-200 bg-neutral-100 p-2 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
<div className="inline-flex items-center gap-1">
|
<div className="inline-flex items-center gap-1">
|
||||||
<MediaUploader editor={editor} />
|
<MediaUploader editor={editor} />
|
||||||
<MentionPopup editor={editor} />
|
<MentionPopup editor={editor} />
|
||||||
@ -150,13 +151,9 @@ export function Composer() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
disabled={editor && editor.isEmpty}
|
disabled={editor && editor.isEmpty}
|
||||||
className="inline-flex h-10 w-20 items-center justify-center rounded-lg bg-blue-500 px-2 font-semibold hover:bg-blue-600 disabled:opacity-50"
|
className="inline-flex h-9 w-20 items-center justify-center rounded-lg bg-blue-500 px-2 font-medium text-white hover:bg-blue-600 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading === true ? (
|
{loading === true ? <LoaderIcon className="h-5 w-5 animate-spin" /> : 'Post'}
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
|
||||||
) : (
|
|
||||||
'Post'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { message } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { UnlistenFn, listen } from '@tauri-apps/api/event';
|
import { UnlistenFn, listen } from '@tauri-apps/api/event';
|
||||||
|
import { message } from '@tauri-apps/plugin-dialog';
|
||||||
import { Editor } from '@tiptap/react';
|
import { Editor } from '@tiptap/react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -53,9 +53,9 @@ export function MediaUploader({ editor }: { editor: Editor }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => uploadToNostrBuild()}
|
onClick={() => uploadToNostrBuild()}
|
||||||
className="ml-2 inline-flex h-10 w-max items-center justify-center gap-1.5 rounded-lg px-2 text-sm font-medium text-white/80 hover:bg-white/10 hover:backdrop-blur-xl"
|
className="ml-2 inline-flex h-10 w-max items-center justify-center gap-1.5 rounded-lg px-2 text-sm font-medium text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<MediaIcon className="h-5 w-5 text-white/80" />
|
<MediaIcon className="h-5 w-5" />
|
||||||
{loading ? 'Uploading...' : 'Add media'}
|
{loading ? 'Uploading...' : 'Add media'}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -19,9 +19,9 @@ export function MentionPopup({ editor }: { editor: Editor }) {
|
|||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg hover:bg-white/10 hover:backdrop-blur-xl"
|
className="inline-flex h-10 w-10 items-center justify-center rounded-lg text-neutral-600 hover:bg-neutral-200 dark:text-neutral-400 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<MentionIcon className="h-5 w-5 text-white/80" />
|
<MentionIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg bg-white/10 backdrop-blur-xl focus:outline-none">
|
<Popover.Content className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg bg-white/10 backdrop-blur-xl focus:outline-none">
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@ -9,19 +8,13 @@ import {
|
|||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ComposeIcon,
|
ComposeIcon,
|
||||||
ExpandIcon,
|
|
||||||
} from '@shared/icons';
|
} from '@shared/icons';
|
||||||
|
|
||||||
import { useComposer } from '@stores/composer';
|
import { useComposer } from '@stores/composer';
|
||||||
|
|
||||||
export function ComposerModal() {
|
export function ComposerModal() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
|
|
||||||
const [toggleModal, open] = useComposer((state) => [state.toggleModal, state.open]);
|
const [toggleModal, open] = useComposer((state) => [state.toggleModal, state.open]);
|
||||||
const [toggleExpand, expand] = useComposer((state) => [
|
|
||||||
state.toggleExpand,
|
|
||||||
state.expand,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={open} onOpenChange={toggleModal}>
|
<Dialog.Root open={open} onOpenChange={toggleModal}>
|
||||||
@ -34,35 +27,21 @@ export function ComposerModal() {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-white dark:bg-black" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-xl dark:bg-white/50" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div
|
<div className="relative h-min w-full max-w-2xl rounded-xl bg-white dark:bg-black">
|
||||||
className={twMerge(
|
|
||||||
'relative h-min w-full rounded-xl bg-neutral-100 dark:bg-neutral-900',
|
|
||||||
expand ? 'max-w-4xl' : 'max-w-2xl'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between px-4 py-4">
|
<div className="flex items-center justify-between px-4 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ComposerUser pubkey={db.account.pubkey} />
|
<ComposerUser pubkey={db.account.pubkey} />
|
||||||
<ChevronRightIcon className="h-4 w-4 text-neutral-400 dark:text-neutral-600" />
|
<ChevronRightIcon className="h-4 w-4 text-neutral-600 dark:text-neutral-400" />
|
||||||
<div className="inline-flex h-7 w-max items-center justify-center gap-0.5 rounded bg-neutral-200 pl-3 pr-1.5 text-sm font-medium text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100">
|
<div className="inline-flex h-7 w-max items-center justify-center gap-0.5 rounded bg-neutral-200 pl-3 pr-1.5 text-sm font-medium text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100">
|
||||||
New Post
|
New Post
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<Dialog.Close className="inline-flex h-10 w-10 items-center justify-center rounded-lg text-neutral-600 hover:bg-neutral-200 hover:text-neutral-500 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-400">
|
||||||
<button
|
<CancelIcon className="h-5 w-5" />
|
||||||
type="button"
|
</Dialog.Close>
|
||||||
onClick={() => toggleExpand()}
|
|
||||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg text-neutral-400 hover:bg-neutral-200 hover:text-neutral-500 dark:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-400"
|
|
||||||
>
|
|
||||||
<ExpandIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
<Dialog.Close className="inline-flex h-10 w-10 items-center justify-center rounded-lg text-neutral-400 hover:bg-neutral-200 hover:text-neutral-500 dark:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-400">
|
|
||||||
<CancelIcon className="h-5 w-5" />
|
|
||||||
</Dialog.Close>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Composer />
|
<Composer />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { displayNpub } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
@ -8,12 +6,12 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Image
|
<img
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-10 w-10 shrink-0 rounded-lg"
|
className="h-10 w-10 shrink-0 rounded-lg"
|
||||||
/>
|
/>
|
||||||
<h5 className="font-medium text-white">
|
<h5 className="font-medium text-neutral-900 dark:text-neutral-100">
|
||||||
{user?.display_name || user?.name || user?.displayName || displayNpub(pubkey, 16)}
|
{user?.display_name || user?.name || user?.displayName || displayNpub(pubkey, 16)}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet, ScrollRestoration } from 'react-router-dom';
|
||||||
|
import { WindowTitlebar } from 'tauri-controls';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
export function NoteLayout() {
|
export function NoteLayout() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-screen">
|
<div className="h-screen w-screen bg-neutral-50 dark:bg-neutral-950">
|
||||||
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
|
{db.platform !== 'macos' ? (
|
||||||
<Outlet />
|
<WindowTitlebar />
|
||||||
|
) : (
|
||||||
|
<div data-tauri-drag-region className="h-9" />
|
||||||
|
)}
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { LogoutIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
export function Logout() {
|
export function Logout() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
|
|
||||||
@ -22,9 +20,9 @@ export function Logout() {
|
|||||||
<AlertDialog.Trigger asChild>
|
<AlertDialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-9 w-9 items-center justify-center rounded-r-lg hover:bg-white/10"
|
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
<LogoutIcon className="h-5 w-5 text-white" />
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</AlertDialog.Trigger>
|
</AlertDialog.Trigger>
|
||||||
<AlertDialog.Portal>
|
<AlertDialog.Portal>
|
||||||
|
@ -61,9 +61,7 @@ export const NIP05 = memo(function NIP05({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={twMerge('inline-flex items-center gap-1', className)}>
|
<div className={twMerge('inline-flex items-center gap-1', className)}>
|
||||||
<div>
|
<p className="text-sm">{nip05}</p>
|
||||||
<p className="text-sm">{nip05}</p>
|
|
||||||
</div>
|
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
{data === true ? (
|
{data === true ? (
|
||||||
<VerifiedIcon className="h-3 w-3 text-green-500" />
|
<VerifiedIcon className="h-3 w-3 text-green-500" />
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { EventPointer } from 'nostr-tools/lib/nip19';
|
import { EventPointer } from 'nostr-tools/lib/types/nip19';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@ -25,19 +24,20 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger asChild>
|
||||||
<HorizontalDotsIcon className="h-5 w-5 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
|
<button type="button" className="inline-flex h-7 w-7 items-center justify-center">
|
||||||
|
<HorizontalDotsIcon className="h-5 w-5 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
|
||||||
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800">
|
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800">
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to={`/notes/text/${id}`}
|
||||||
onClick={() => copyLink()}
|
|
||||||
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-700"
|
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||||
>
|
>
|
||||||
Focus
|
Focus
|
||||||
</button>
|
</Link>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
|
@ -62,7 +62,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
|
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
{reaction ? (
|
{reaction ? (
|
||||||
<img src={getReactionImage(reaction)} alt={reaction} className="h-5 w-5" />
|
<img src={getReactionImage(reaction)} alt={reaction} className="h-5 w-5" />
|
||||||
|
@ -21,7 +21,7 @@ export function NoteReply({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setReply(id, pubkey, root)}
|
onClick={() => setReply(id, pubkey, root)}
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
|
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<ReplyIcon className="h-5 w-5 group-hover:text-blue-500" />
|
<ReplyIcon className="h-5 w-5 group-hover:text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -44,7 +44,7 @@ export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
<AlertDialog.Trigger asChild>
|
<AlertDialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
|
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<RepostIcon
|
<RepostIcon
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { webln } from '@getalby/sdk';
|
import { webln } from '@getalby/sdk';
|
||||||
import { SendPaymentResponse } from '@getalby/sdk/dist/types';
|
import { SendPaymentResponse } from '@getalby/sdk/dist/types';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { message } from '@tauri-apps/plugin-dialog';
|
import { message } from '@tauri-apps/plugin-dialog';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
@ -79,9 +80,10 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getWalletConnectURL() {
|
async function getWalletConnectURL() {
|
||||||
// const uri: string = await invoke('secure_load', { key: 'nwc' });
|
const uri: string = await invoke('secure_load', { key: 'nwc' });
|
||||||
setWalletConnectURL('todo');
|
if (uri) setWalletConnectURL(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWalletConnectURL();
|
getWalletConnectURL();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -97,7 +99,7 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
|
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<ZapIcon className="h-5 w-5 group-hover:text-blue-500" />
|
<ZapIcon className="h-5 w-5 group-hover:text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -80,7 +80,7 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
|
|||||||
<>
|
<>
|
||||||
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.6rem)] w-0.5 bg-gradient-to-t from-black/20 to-black/10 dark:from-white/20 dark:to-white/10" />
|
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.6rem)] w-0.5 bg-gradient-to-t from-black/20 to-black/10 dark:from-white/20 dark:to-white/10" />
|
||||||
<div className="mb-5 flex flex-col">
|
<div className="mb-5 flex flex-col">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
import { Boost, Hashtag, Invoice, MentionUser } from '@shared/notes';
|
||||||
|
|
||||||
export function ArticleDetailNote({ event }: { event: NDKEvent }) {
|
export function ArticleDetailNote({ event }: { event: NDKEvent }) {
|
||||||
/*const metadata = useMemo(() => {
|
/*const metadata = useMemo(() => {
|
||||||
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
|
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
|
||||||
@ -26,7 +29,38 @@ export function ArticleDetailNote({ event }: { event: NDKEvent }) {
|
|||||||
}, [event.id]);*/
|
}, [event.id]);*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown className="markdown-article" remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown
|
||||||
|
className="prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500"
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
a: ({ href }) => {
|
||||||
|
const cleanURL = new URL(href);
|
||||||
|
cleanURL.search = '';
|
||||||
|
return (
|
||||||
|
<Link to={href} target="_blank" className="w-max break-all hover:!underline">
|
||||||
|
{cleanURL.hostname + cleanURL.pathname}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
del: ({ children }) => {
|
||||||
|
const key = children[0] as string;
|
||||||
|
if (typeof key !== 'string') return;
|
||||||
|
if (key.startsWith('pub') && key.length > 50 && key.length < 100) {
|
||||||
|
return <MentionUser pubkey={key.replace('pub-', '')} />;
|
||||||
|
}
|
||||||
|
if (key.startsWith('tag')) {
|
||||||
|
return <Hashtag tag={key.replace('tag-', '')} />;
|
||||||
|
}
|
||||||
|
if (key.startsWith('boost')) {
|
||||||
|
return <Boost boost={key.replace('boost-', '')} />;
|
||||||
|
}
|
||||||
|
if (key.startsWith('lnbc')) {
|
||||||
|
return <Invoice invoice={key.replace('lnbc-', '')} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
linkTarget={'_blank'}
|
||||||
|
>
|
||||||
{event.content}
|
{event.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
);
|
);
|
||||||
|
@ -70,7 +70,11 @@ export function Repost({
|
|||||||
>
|
>
|
||||||
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
|
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
|
||||||
<div className="relative flex flex-col">
|
<div className="relative flex flex-col">
|
||||||
<User pubkey={embedEvent.pubkey} time={embedEvent.created_at} />
|
<User
|
||||||
|
pubkey={embedEvent.pubkey}
|
||||||
|
time={embedEvent.created_at}
|
||||||
|
eventId={embedEvent.id}
|
||||||
|
/>
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
@ -147,7 +151,7 @@ export function Repost({
|
|||||||
>
|
>
|
||||||
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
|
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
|
||||||
<div className="relative flex flex-col">
|
<div className="relative flex flex-col">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
|
@ -16,13 +16,13 @@ export function ImagePreview({ urls }: { urls: string[] }) {
|
|||||||
<div key={url} className="group relative">
|
<div key={url} className="group relative">
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt="image"
|
alt={url}
|
||||||
className="h-auto w-full rounded-lg border border-neutral-200 object-cover dark:border-neutral-800"
|
className="h-auto w-full rounded-lg border border-neutral-300 object-cover dark:border-neutral-700"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => downloadImage(url)}
|
onClick={() => downloadImage(url)}
|
||||||
className="absolute right-2 top-2 hidden h-8 w-8 items-center justify-center rounded-md bg-black/50 backdrop-blur-md group-hover:inline-flex hover:bg-black/40"
|
className="absolute right-2 top-2 hidden h-10 w-10 items-center justify-center rounded-lg bg-black/50 backdrop-blur-xl group-hover:inline-flex hover:bg-blue-500"
|
||||||
>
|
>
|
||||||
<DownloadIcon className="h-5 w-5 text-white" />
|
<DownloadIcon className="h-5 w-5 text-white" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Button } from '@shared/button';
|
import { Button } from '@shared/button';
|
||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
@ -24,34 +23,32 @@ export function NoteReplyForm({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 flex flex-col rounded-xl bg-white/10 backdrop-blur-xl">
|
<div className="mt-3 flex flex-col rounded-xl bg-neutral-200 dark:bg-neutral-800">
|
||||||
<div className="relative w-full flex-1 overflow-hidden">
|
<div className="relative w-full flex-1 overflow-hidden">
|
||||||
<textarea
|
<textarea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder="Reply to this thread..."
|
placeholder="Reply to this thread..."
|
||||||
className=" relative h-24 w-full resize-none rounded-md bg-transparent px-3 py-3 text-base text-white !outline-none placeholder:text-white/50"
|
className=" relative h-24 w-full resize-none rounded-md bg-transparent px-3 py-3 text-base text-neutral-900 !outline-none placeholder:text-neutral-600 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full border-t border-white/10 px-3 py-3">
|
<div className="w-full border-t border-neutral-300 px-3 py-3 dark:border-neutral-700">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div>
|
<div>Loading</div>
|
||||||
<p>Loading...</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="inline-flex items-center gap-3">
|
<div className="inline-flex items-center gap-2">
|
||||||
<div className="relative h-11 w-11 shrink-0 rounded">
|
<div className="relative h-11 w-11 shrink-0 rounded">
|
||||||
<Image
|
<img
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-11 w-11 rounded-lg bg-white object-cover"
|
className="h-11 w-11 rounded-lg bg-white object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 text-sm leading-none text-white/50">Reply as</p>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">Reply as</p>
|
||||||
<p className="text-sm font-medium leading-none text-white">
|
<p className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
{user?.name || displayNpub(pubkey, 16)}
|
{user?.name || displayNpub(pubkey, 16)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,7 +30,7 @@ export function NoteWrapper({
|
|||||||
<div className="relative">{root && <ChildNote id={root} />}</div>
|
<div className="relative">{root && <ChildNote id={root} />}</div>
|
||||||
<div className="relative">{reply && <ChildNote id={reply} root={root} />}</div>
|
<div className="relative">{reply && <ChildNote id={reply} root={root} />}</div>
|
||||||
<div className="relative flex flex-col">
|
<div className="relative flex flex-col">
|
||||||
<User pubkey={event.pubkey} time={event.created_at} />
|
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||||
<div className="-mt-4 flex items-start gap-3">
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
<div className="w-10 shrink-0" />
|
<div className="w-10 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
|
@ -15,12 +15,12 @@ export function TitleBar({ id, title }: { id?: string; title?: string }) {
|
|||||||
{id === '9999' ? (
|
{id === '9999' ? (
|
||||||
<div className="isolate flex -space-x-2">
|
<div className="isolate flex -space-x-2">
|
||||||
{db.account.circles
|
{db.account.circles
|
||||||
?.slice(0, 4)
|
?.slice(0, 10)
|
||||||
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
|
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
|
||||||
{db.account.circles?.length > 4 ? (
|
{db.account.circles?.length > 10 ? (
|
||||||
<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">
|
<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">
|
<span className="text-xs font-medium">
|
||||||
+{db.account.circles?.length - 4}
|
+{db.account.circles?.length - 10}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -216,13 +216,13 @@ export const User = memo(function User({
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
style={{ contentVisibility: 'auto' }}
|
style={{ contentVisibility: 'auto' }}
|
||||||
className="inline-block h-8 w-8 rounded-full ring-1 ring-black"
|
className="inline-block h-8 w-8 rounded-full ring-1 ring-neutral-200 dark:ring-neutral-800"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={svgURI}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="inline-block h-8 w-8 rounded-full bg-black ring-1 ring-black dark:bg-white"
|
className="inline-block h-8 w-8 rounded-full bg-black ring-1 ring-neutral-200 dark:bg-white dark:ring-neutral-800"
|
||||||
/>
|
/>
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
@ -313,7 +313,7 @@ export const User = memo(function User({
|
|||||||
<h5 className="max-w-[15rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
<h5 className="max-w-[15rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
{user?.name || user?.display_name || user?.displayName || 'Anon'}
|
{user?.name || user?.display_name || user?.displayName || 'Anon'}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="inline-flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-300">
|
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<span>{createdAt}</span>
|
<span>{createdAt}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{displayNpub(pubkey, 16)}</span>
|
<span>{displayNpub(pubkey, 16)}</span>
|
||||||
|
@ -113,7 +113,7 @@ export function LocalNetworkWidget() {
|
|||||||
) : dbEvents.length === 0 ? (
|
) : dbEvents.length === 0 ? (
|
||||||
<EventLoader firstTime={true} />
|
<EventLoader firstTime={true} />
|
||||||
) : (
|
) : (
|
||||||
<VList className="h-full scrollbar-none" shift={true}>
|
<VList className="h-full scrollbar-none">
|
||||||
{!isFetched ? <EventLoader firstTime={false} /> : null}
|
{!isFetched ? <EventLoader firstTime={false} /> : null}
|
||||||
{dbEvents.map((item) => renderItem(item))}
|
{dbEvents.map((item) => renderItem(item))}
|
||||||
<div className="flex items-center justify-center px-3 py-1.5">
|
<div className="flex items-center justify-center px-3 py-1.5">
|
||||||
|
@ -202,8 +202,31 @@ export function useNostr() {
|
|||||||
|
|
||||||
const getAllEventsSinceLastLogin = async (customSince?: number) => {
|
const getAllEventsSinceLastLogin = async (customSince?: number) => {
|
||||||
try {
|
try {
|
||||||
let since: number;
|
|
||||||
const dbEventsEmpty = await db.isEventsEmpty();
|
const dbEventsEmpty = await db.isEventsEmpty();
|
||||||
|
const circleSetting = await db.getSettingValue('circles');
|
||||||
|
|
||||||
|
const user = ndk.getUser({ hexpubkey: db.account.pubkey });
|
||||||
|
const follows = await user.follows();
|
||||||
|
const relayList = await user.relayList();
|
||||||
|
|
||||||
|
const followsAsArr = [];
|
||||||
|
follows.forEach((user) => {
|
||||||
|
followsAsArr.push(user.pubkey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// update user's follows
|
||||||
|
await db.updateAccount('follows', JSON.stringify(followsAsArr));
|
||||||
|
if (circleSetting !== '1')
|
||||||
|
await db.updateAccount('circles', JSON.stringify(followsAsArr));
|
||||||
|
|
||||||
|
// update user's relay list
|
||||||
|
if (relayList) {
|
||||||
|
for (const relay of relayList.relays) {
|
||||||
|
await db.createRelay(relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let since: number;
|
||||||
|
|
||||||
if (!customSince) {
|
if (!customSince) {
|
||||||
if (dbEventsEmpty || db.account.last_login_at === 0) {
|
if (dbEventsEmpty || db.account.last_login_at === 0) {
|
||||||
|
Loading…
Reference in New Issue
Block a user