mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
add mobile profile button
change database
This commit is contained in:
parent
7c30160b38
commit
2450ecd0d4
@ -169,3 +169,9 @@ export const CheckIcon = createIcon({
|
||||
d: "M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const NotificationIcon = createIcon({
|
||||
displayName: "NotificationIcon",
|
||||
d: "M5 18h14v-6.969C19 7.148 15.866 4 12 4s-7 3.148-7 7.031V18zm7-16c4.97 0 9 4.043 9 9.031V20H3v-8.969C3 6.043 7.03 2 12 2zM9.5 21h5a2.5 2.5 0 1 1-5 0z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -1,7 +1,26 @@
|
||||
import React from "react";
|
||||
import { Avatar, Button, Container, Flex, Heading, IconButton, LinkOverlay, Text, VStack } from "@chakra-ui/react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
LinkOverlay,
|
||||
Text,
|
||||
VStack,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
MenuItemOption,
|
||||
MenuGroup,
|
||||
MenuOptionGroup,
|
||||
MenuDivider,
|
||||
Box,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { FeedIcon, LogoutIcon, ProfileIcon, SettingsIcon } from "./icons";
|
||||
import { FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, SettingsIcon } from "./icons";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { ConnectedRelays } from "./connected-relays";
|
||||
|
||||
@ -12,87 +31,128 @@ import { ReloadPrompt } from "./reload-prompt";
|
||||
import { PostModalProvider } from "../providers/post-modal-provider";
|
||||
import { useReadonlyMode } from "../hooks/use-readonly-mode";
|
||||
import { ProfileButton } from "./profile-button";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
const MobileProfileHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
|
||||
return (
|
||||
<Flex justifyContent="space-between" px="2" pt="2">
|
||||
<Menu>
|
||||
<MenuButton as={Box}>
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem icon={<ProfileIcon />} as={Link} to={`/u/${pubkey}`}>
|
||||
Profile
|
||||
</MenuItem>
|
||||
<MenuItem icon={<LogoutIcon />} onClick={() => identity.logout()}>
|
||||
Logout
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
icon={<NotificationIcon />}
|
||||
aria-label="Notifications"
|
||||
title="Notifications"
|
||||
size="sm"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileBottomNav = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Flex flexShrink={0} gap="2" padding="2">
|
||||
<IconButton icon={<FeedIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="lg" />
|
||||
<IconButton
|
||||
icon={<ProfileIcon />}
|
||||
aria-label="Profile"
|
||||
onClick={() => navigate(`/profile`)}
|
||||
flexGrow="1"
|
||||
size="lg"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
aria-label="Settings"
|
||||
onClick={() => navigate("/settings")}
|
||||
flexGrow="1"
|
||||
size="lg"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopSideNav = () => {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const readonly = useReadonlyMode();
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Flex direction="column" height="100%">
|
||||
<ReloadPrompt />
|
||||
<Flex flexGrow={1} direction="column" overflow="hidden">
|
||||
<ErrorBoundary>
|
||||
<PostModalProvider>{children}</PostModalProvider>
|
||||
</ErrorBoundary>
|
||||
</Flex>
|
||||
<Flex flexShrink={0} gap="2" padding="2">
|
||||
<IconButton icon={<FeedIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="lg" />
|
||||
<IconButton
|
||||
icon={<ProfileIcon />}
|
||||
aria-label="Profile"
|
||||
onClick={() => navigate(`/profile`)}
|
||||
flexGrow="1"
|
||||
size="lg"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
aria-label="Settings"
|
||||
onClick={() => navigate("/settings")}
|
||||
flexGrow="1"
|
||||
size="lg"
|
||||
/>
|
||||
</Flex>
|
||||
return (
|
||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||
<Flex gap="2" alignItems="center" position="relative">
|
||||
<LinkOverlay as={Link} to="/" />
|
||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||
<Heading size="md">noStrudel</Heading>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
<ProfileButton />
|
||||
<Button onClick={() => navigate("/")} leftIcon={<FeedIcon />}>
|
||||
Home
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||
Settings
|
||||
</Button>
|
||||
<Button onClick={() => identity.logout()} leftIcon={<LogoutIcon />}>
|
||||
Logout
|
||||
</Button>
|
||||
{readonly && (
|
||||
<Text color="yellow.500" textAlign="center">
|
||||
Readonly Mode
|
||||
</Text>
|
||||
)}
|
||||
<ConnectedRelays />
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
const FollowingSideNav = () => {
|
||||
return (
|
||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||
<Heading size="md">Following</Heading>
|
||||
<FollowingList />
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Container
|
||||
size="lg"
|
||||
display="flex"
|
||||
gap="2"
|
||||
flexDirection="column"
|
||||
height="100vh"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
padding="0"
|
||||
>
|
||||
<ReloadPrompt />
|
||||
{isMobile && <MobileProfileHeader />}
|
||||
<Flex gap="4" grow={1} overflow="hidden">
|
||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||
<Flex gap="2" alignItems="center" position="relative">
|
||||
<LinkOverlay as={Link} to="/" />
|
||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||
<Heading size="md">noStrudel</Heading>
|
||||
</Flex>
|
||||
<ProfileButton />
|
||||
<Button onClick={() => navigate("/")} leftIcon={<FeedIcon />}>
|
||||
Home
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||
Settings
|
||||
</Button>
|
||||
<Button onClick={() => identity.logout()} leftIcon={<LogoutIcon />}>
|
||||
Logout
|
||||
</Button>
|
||||
{readonly && (
|
||||
<Text color="yellow.500" textAlign="center">
|
||||
Readonly Mode
|
||||
</Text>
|
||||
)}
|
||||
<ConnectedRelays />
|
||||
</VStack>
|
||||
{!isMobile && <DesktopSideNav />}
|
||||
<Flex flexGrow={1} direction="column" overflow="hidden">
|
||||
<ErrorBoundary>
|
||||
<PostModalProvider>{children}</PostModalProvider>
|
||||
</ErrorBoundary>
|
||||
</Flex>
|
||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||
<Heading size="md">Following</Heading>
|
||||
<FollowingList />
|
||||
</VStack>
|
||||
{!isMobile && <FollowingSideNav />}
|
||||
</Flex>
|
||||
{isMobile && <MobileBottomNav />}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
@ -3,6 +3,8 @@ import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
import "./services/pubkey-relay-weights";
|
||||
|
||||
const element = document.getElementById("root");
|
||||
if (!element) throw new Error("missing mount point");
|
||||
const root = createRoot(element);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { openDB } from "idb";
|
||||
import { openDB, deleteDB } from "idb";
|
||||
|
||||
import { IDBPDatabase, IDBPTransaction, StoreNames } from "idb";
|
||||
import { CustomSchema } from "./schema";
|
||||
@ -12,29 +12,32 @@ type MigrationFunction = (
|
||||
const MIGRATIONS: MigrationFunction[] = [
|
||||
// 0 -> 1
|
||||
function (db, transaction, event) {
|
||||
const metadata = db.createObjectStore("user-metadata", {
|
||||
const metadata = db.createObjectStore("userMetadata", {
|
||||
keyPath: "pubkey",
|
||||
});
|
||||
|
||||
const contacts = db.createObjectStore("user-contacts", {
|
||||
const contacts = db.createObjectStore("userContacts", {
|
||||
keyPath: "pubkey",
|
||||
});
|
||||
contacts.createIndex("created_at", "created_at");
|
||||
contacts.createIndex("contacts", "contacts", { multiEntry: true });
|
||||
|
||||
const dnsIdentifiers = db.createObjectStore("dns-identifiers");
|
||||
const dnsIdentifiers = db.createObjectStore("dnsIdentifiers");
|
||||
dnsIdentifiers.createIndex("pubkey", "pubkey", { unique: false });
|
||||
dnsIdentifiers.createIndex("name", "name", { unique: false });
|
||||
dnsIdentifiers.createIndex("domain", "domain", { unique: false });
|
||||
dnsIdentifiers.createIndex("updated", "updated", { unique: false });
|
||||
|
||||
const pubkeyRelayWeights = db.createObjectStore("pubkeyRelayWeights", { keyPath: "pubkey" });
|
||||
|
||||
// setup data
|
||||
const settings = db.createObjectStore("settings");
|
||||
},
|
||||
];
|
||||
|
||||
const dbName = "storage";
|
||||
const version = 1;
|
||||
const db = await openDB<CustomSchema>("storage", version, {
|
||||
const db = await openDB<CustomSchema>(dbName, version, {
|
||||
upgrade(db, oldVersion, newVersion, transaction, event) {
|
||||
// TODO: why is newVersion sometimes null?
|
||||
// @ts-ignore
|
||||
@ -47,10 +50,16 @@ const db = await openDB<CustomSchema>("storage", version, {
|
||||
},
|
||||
});
|
||||
|
||||
export async function clearData() {
|
||||
await db.clear("user-metadata");
|
||||
await db.clear("user-contacts");
|
||||
await db.clear("dns-identifiers");
|
||||
export async function clearCacheData() {
|
||||
await db.clear("userMetadata");
|
||||
await db.clear("userContacts");
|
||||
await db.clear("dnsIdentifiers");
|
||||
await db.clear("pubkeyRelayWeights");
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export async function deleteDatabase() {
|
||||
await deleteDB(dbName);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
|
@ -2,29 +2,31 @@ import { DBSchema } from "idb";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
export interface CustomSchema extends DBSchema {
|
||||
"user-metadata": {
|
||||
userMetadata: {
|
||||
key: string;
|
||||
value: NostrEvent;
|
||||
};
|
||||
"user-contacts": {
|
||||
userContacts: {
|
||||
key: string;
|
||||
value: {
|
||||
pubkey: string;
|
||||
relays: Record<string, { read: boolean; write: boolean }>;
|
||||
contacts: string[];
|
||||
// contacts: {
|
||||
// pubkey: string;
|
||||
// relay?: string;
|
||||
// }[];
|
||||
contactRelay: Record<string, string | undefined>;
|
||||
created_at: number;
|
||||
};
|
||||
indexes: { created_at: number; contacts: string };
|
||||
};
|
||||
"dns-identifiers": {
|
||||
dnsIdentifiers: {
|
||||
key: string;
|
||||
value: { name: string; domain: string; pubkey: string; relays: string[]; updated: number };
|
||||
indexes: { name: string; domain: string; pubkey: string; updated: number };
|
||||
};
|
||||
pubkeyRelayWeights: {
|
||||
key: string;
|
||||
value: { pubkey: string; relays: Record<string, number>; updated: number };
|
||||
indexes: { pubkey: string };
|
||||
};
|
||||
settings: {
|
||||
key: string;
|
||||
value: any;
|
||||
|
@ -47,14 +47,14 @@ async function addToCache(domain: string, json: IdentityJson) {
|
||||
for (const name of Object.keys(json.names)) {
|
||||
const identity = getIdentityFromJson(name, domain, json);
|
||||
if (identity) {
|
||||
wait.push(db.put("dns-identifiers", { ...identity, updated: now }, `${name}@${domain}`));
|
||||
wait.push(db.put("dnsIdentifiers", { ...identity, updated: now }, `${name}@${domain}`));
|
||||
}
|
||||
}
|
||||
await Promise.all(wait);
|
||||
}
|
||||
|
||||
async function getIdentity(address: string) {
|
||||
const cached = await db.get("dns-identifiers", address);
|
||||
const cached = await db.get("dnsIdentifiers", address);
|
||||
if (cached) return cached;
|
||||
|
||||
// TODO: if it fails, maybe cache a failure message
|
||||
@ -63,13 +63,13 @@ async function getIdentity(address: string) {
|
||||
|
||||
async function pruneCache() {
|
||||
const keys = await db.getAllKeysFromIndex(
|
||||
"dns-identifiers",
|
||||
"dnsIdentifiers",
|
||||
"updated",
|
||||
IDBKeyRange.upperBound(moment().subtract(1, "hour").unix())
|
||||
);
|
||||
|
||||
for (const pubkey of keys) {
|
||||
db.delete("dns-identifiers", pubkey);
|
||||
db.delete("dnsIdentifiers", pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
|
70
src/services/pubkey-relay-weights.ts
Normal file
70
src/services/pubkey-relay-weights.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import moment from "moment";
|
||||
import db from "./db";
|
||||
import { UserContacts } from "./user-contacts";
|
||||
|
||||
const changed = new Set();
|
||||
const cache: Record<string, Record<string, number> | undefined> = {};
|
||||
|
||||
async function populateCacheFromDb(pubkey: string) {
|
||||
if (!cache[pubkey]) {
|
||||
cache[pubkey] = (await db.get("pubkeyRelayWeights", pubkey))?.relays;
|
||||
}
|
||||
}
|
||||
|
||||
async function addWeight(pubkey: string, relay: string, weight: number = 1) {
|
||||
await populateCacheFromDb(pubkey);
|
||||
|
||||
const relays = cache[pubkey] || (cache[pubkey] = {});
|
||||
|
||||
if (relays[relay]) {
|
||||
relays[relay] += weight;
|
||||
} else {
|
||||
relays[relay] = weight;
|
||||
}
|
||||
changed.add(pubkey);
|
||||
}
|
||||
|
||||
async function saveCache() {
|
||||
const now = moment().unix();
|
||||
const transaction = db.transaction("pubkeyRelayWeights", "readwrite");
|
||||
|
||||
for (const [pubkey, relays] of Object.entries(cache)) {
|
||||
if (changed.has(pubkey)) {
|
||||
if (relays) transaction.store?.put({ pubkey, relays, updated: now });
|
||||
}
|
||||
}
|
||||
changed.clear();
|
||||
}
|
||||
|
||||
async function handleContactList(contacts: UserContacts) {
|
||||
// save the relays for contacts
|
||||
for (const [pubkey, relay] of Object.entries(contacts.contactRelay)) {
|
||||
if (relay) await addWeight(pubkey, relay);
|
||||
}
|
||||
|
||||
// save this pubkeys relays
|
||||
for (const [relay, opts] of Object.entries(contacts.relays)) {
|
||||
// only save relays this users writes to
|
||||
if (opts.write) {
|
||||
await addWeight(contacts.pubkey, relay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getPubkeyRelays(pubkey: string) {
|
||||
await populateCacheFromDb(pubkey);
|
||||
return cache[pubkey] || {};
|
||||
}
|
||||
const pubkeyRelayWeightsService = {
|
||||
handleContactList,
|
||||
getPubkeyRelays,
|
||||
};
|
||||
|
||||
setInterval(() => saveCache(), 1000);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.pubkeyRelayWeightsService = pubkeyRelayWeightsService;
|
||||
}
|
||||
|
||||
export default pubkeyRelayWeightsService;
|
@ -1,4 +1,4 @@
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { isPTag, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
|
||||
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||
@ -6,6 +6,7 @@ import { safeJson } from "../helpers/parse";
|
||||
import db from "./db";
|
||||
import settings from "./settings";
|
||||
import userFollowersService from "./user-followers";
|
||||
import pubkeyRelayWeightsService from "./pubkey-relay-weights";
|
||||
|
||||
const subscription = new NostrSubscription([], undefined, "user-contacts");
|
||||
const subjects = new PubkeySubjectCache<UserContacts>();
|
||||
@ -15,24 +16,25 @@ export type UserContacts = {
|
||||
pubkey: string;
|
||||
relays: Record<string, { read: boolean; write: boolean }>;
|
||||
contacts: string[];
|
||||
// contacts: {
|
||||
// pubkey: string;
|
||||
// relay?: string;
|
||||
// }[];
|
||||
contactRelay: Record<string, string | undefined>;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
function parseContacts(event: NostrEvent): UserContacts {
|
||||
// const keys = event.tags
|
||||
// .filter((tag) => tag[0] === "p" && tag[1])
|
||||
// .map((tag) => ({ pubkey: tag[1] as string, relay: tag[2] }));
|
||||
const keys = event.tags.filter((tag) => tag[0] === "p" && tag[1]).map((tag) => tag[1]) as string[];
|
||||
const relays = safeJson(event.content, {}) as UserContacts["relays"];
|
||||
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
|
||||
const contactRelay = event.tags.filter(isPTag).reduce((dir, tag) => {
|
||||
if (tag[2]) {
|
||||
dir[tag[1]] = tag[2];
|
||||
}
|
||||
return dir;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
return {
|
||||
pubkey: event.pubkey,
|
||||
relays,
|
||||
contacts: keys,
|
||||
contacts: pubkeys,
|
||||
contactRelay,
|
||||
created_at: event.created_at,
|
||||
};
|
||||
}
|
||||
@ -45,7 +47,7 @@ function requestContacts(pubkey: string, relays: string[] = [], alwaysRequest =
|
||||
if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
if (!subject.value) {
|
||||
db.get("user-contacts", pubkey).then((cached) => {
|
||||
db.get("userContacts", pubkey).then((cached) => {
|
||||
if (cached) subject.next(cached);
|
||||
});
|
||||
}
|
||||
@ -81,21 +83,26 @@ function flushRequests() {
|
||||
function receiveEvent(event: NostrEvent) {
|
||||
if (event.kind !== 3) return;
|
||||
|
||||
const parsed = parseContacts(event);
|
||||
|
||||
if (subjects.hasSubject(event.pubkey)) {
|
||||
const subject = subjects.getSubject(event.pubkey);
|
||||
const latest = subject.getValue();
|
||||
// make sure the event is newer than whats in the subject
|
||||
if (!latest || event.created_at > latest.created_at) {
|
||||
const parsed = parseContacts(event);
|
||||
subject.next(parsed);
|
||||
// send it to the db
|
||||
db.put("user-contacts", parsed);
|
||||
db.put("userContacts", parsed);
|
||||
// add it to the pubkey relay weights
|
||||
pubkeyRelayWeightsService.handleContactList(parsed);
|
||||
}
|
||||
} else {
|
||||
db.get("user-contacts", event.pubkey).then((cached) => {
|
||||
db.get("userContacts", event.pubkey).then((cached) => {
|
||||
// make sure the event is newer than whats in the db
|
||||
if (!cached || event.created_at > cached.created_at) {
|
||||
db.put("user-contacts", parseContacts(event));
|
||||
db.put("userContacts", parsed);
|
||||
// add it to the pubkey relay weights
|
||||
pubkeyRelayWeightsService.handleContactList(parsed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ function requestFollowers(pubkey: string, relays: string[] = [], alwaysRequest =
|
||||
|
||||
if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
|
||||
db.getAllKeysFromIndex("user-contacts", "contacts", pubkey).then((cached) => {
|
||||
db.getAllKeysFromIndex("userContacts", "contacts", pubkey).then((cached) => {
|
||||
mergeNext(subject, cached);
|
||||
});
|
||||
|
||||
|
@ -21,7 +21,7 @@ function requestMetadata(pubkey: string, relays: string[], alwaysRequest = false
|
||||
}
|
||||
|
||||
if (!subject.value) {
|
||||
db.get("user-metadata", pubkey).then((cached) => {
|
||||
db.get("userMetadata", pubkey).then((cached) => {
|
||||
if (cached) {
|
||||
const parsed = parseMetadata(cached);
|
||||
if (parsed) subject.next(parsed);
|
||||
@ -70,7 +70,7 @@ subscription.onEvent.subscribe((event) => {
|
||||
const parsed = parseMetadata(event);
|
||||
if (parsed) {
|
||||
subject.next(parsed);
|
||||
db.put("user-metadata", event);
|
||||
db.put("userMetadata", event);
|
||||
forceRequestedKeys.delete(event.pubkey);
|
||||
}
|
||||
}
|
||||
|
@ -39,8 +39,8 @@ export type Kind0ParsedContent = {
|
||||
};
|
||||
|
||||
export function isETag(tag: Tag): tag is ETag {
|
||||
return tag[0] === "e";
|
||||
return tag[0] === "e" && tag[1] !== undefined;
|
||||
}
|
||||
export function isPTag(tag: Tag): tag is PTag {
|
||||
return tag[0] === "p";
|
||||
return tag[0] === "p" && tag[1] !== undefined;
|
||||
}
|
||||
|
@ -20,15 +20,17 @@ import {
|
||||
AccordionButton,
|
||||
Box,
|
||||
AccordionIcon,
|
||||
ButtonGroup,
|
||||
} from "@chakra-ui/react";
|
||||
import { SyntheticEvent, useState } from "react";
|
||||
import { GlobalIcon, TrashIcon } from "../../components/icons";
|
||||
import { RelayStatus } from "./relay-status";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import settings from "../../services/settings";
|
||||
import { clearData } from "../../services/db";
|
||||
import { clearCacheData, deleteDatabase } from "../../services/db";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
export const SettingsView = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -54,10 +56,17 @@ export const SettingsView = () => {
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const handleClearData = async () => {
|
||||
setClearing(true);
|
||||
await clearData();
|
||||
await clearCacheData();
|
||||
setClearing(false);
|
||||
};
|
||||
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const handleDeleteDatabase = async () => {
|
||||
setDeleting(true);
|
||||
await deleteDatabase();
|
||||
setDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="2" overflow="auto">
|
||||
<Accordion defaultIndex={[0]} allowMultiple>
|
||||
@ -190,9 +199,14 @@ export const SettingsView = () => {
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<Button colorScheme="red" onClick={handleClearData} isLoading={clearing} isDisabled={clearing}>
|
||||
Remove All Data
|
||||
</Button>
|
||||
<ButtonGroup>
|
||||
<Button onClick={handleClearData} isLoading={clearing} isDisabled={clearing}>
|
||||
Clear cache data
|
||||
</Button>
|
||||
<Button colorScheme="red" onClick={handleDeleteDatabase} isLoading={deleting} isDisabled={deleting}>
|
||||
Delete database
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, Flex, Heading, IconButton, Link } from "@chakra-ui/react";
|
||||
import { Box, Flex, Heading, IconButton, Link, Text } from "@chakra-ui/react";
|
||||
import { Link as ReactRouterLink } from "react-router-dom";
|
||||
|
||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||
@ -7,16 +7,19 @@ import { AddIcon } from "../../../components/icons";
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip-19";
|
||||
|
||||
export const UserCard = ({ pubkey }: { pubkey: string }) => {
|
||||
export const UserCard = ({ pubkey, relay }: { pubkey: string; relay?: string }) => {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<Box borderWidth="1px" borderRadius="lg" pl="3" pr="3" pt="2" pb="2" overflow="hidden">
|
||||
<Flex gap="4" alignItems="center">
|
||||
<UserAvatar pubkey={pubkey} />
|
||||
<Link as={ReactRouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
<Heading size="sm">{getUserDisplayName(metadata, pubkey)}</Heading>
|
||||
</Link>
|
||||
<Box>
|
||||
<Link as={ReactRouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
<Heading size="sm">{getUserDisplayName(metadata, pubkey)}</Heading>
|
||||
</Link>
|
||||
{relay && <Text>{relay}</Text>}
|
||||
</Box>
|
||||
<IconButton size="sm" icon={<AddIcon />} aria-label="Follow user" title="Follow" ml="auto" />
|
||||
</Flex>
|
||||
</Box>
|
||||
|
@ -19,7 +19,7 @@ const UserFollowingTab = () => {
|
||||
<>
|
||||
<Grid templateColumns={{ base: "1fr", xl: "repeat(2, 1fr)", "2xl": "repeat(3, 1fr)" }} gap="2">
|
||||
{pagination.pageItems.map((pubkey, i) => (
|
||||
<UserCard key={pubkey + i} pubkey={pubkey} />
|
||||
<UserCard key={pubkey + i} pubkey={pubkey} relay={contacts.contactRelay[pubkey]} />
|
||||
))}
|
||||
</Grid>
|
||||
<PaginationControls {...pagination} buttonSize="sm" />
|
||||
|
@ -24,7 +24,7 @@ import { UserTipButton } from "../../components/user-tip-button";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity";
|
||||
import { truncatedId } from "../../helpers/nostr-event";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||
import { ClipboardIcon, KeyIcon } from "../../components/icons";
|
||||
import { KeyIcon } from "../../components/icons";
|
||||
import { CopyIconButton } from "../../components/copy-icon-button";
|
||||
|
||||
const tabs = [
|
||||
@ -54,7 +54,7 @@ const UserView = () => {
|
||||
<UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} />
|
||||
<Flex direction="column" gap={isMobile ? 0 : 2} grow="1" overflow="hidden">
|
||||
<Flex gap="2" justifyContent="space-between" width="100%">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<Heading size={isMobile ? "md" : "lg"}>{getUserDisplayName(metadata, pubkey)}</Heading>
|
||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||
</Flex>
|
||||
@ -101,7 +101,7 @@ const UserView = () => {
|
||||
index={activeTab}
|
||||
onChange={(v) => navigate(tabs[v].path)}
|
||||
>
|
||||
<TabList overflow={isMobile ? "auto" : undefined}>
|
||||
<TabList overflowY="auto">
|
||||
{tabs.map(({ label }) => (
|
||||
<Tab key={label}>{label}</Tab>
|
||||
))}
|
||||
|
Loading…
x
Reference in New Issue
Block a user