redesign widget list

This commit is contained in:
Ren Amamiya 2023-09-06 14:30:57 +07:00
parent 09aea3cff5
commit 5c8850ea8f
10 changed files with 152 additions and 56 deletions

View File

@ -2,9 +2,17 @@ import { useCallback } from 'react';
import { useStorage } from '@libs/storage/provider';
import {
ArticleIcon,
FileIcon,
FollowsIcon,
GroupFeedsIcon,
HashtagIcon,
TrendingIcon,
} from '@shared/icons';
import { TitleBar } from '@shared/titleBar';
import { DefaultWidgets, useWidgets } from '@stores/widgets';
import { DefaultWidgets, WidgetKinds, useWidgets } from '@stores/widgets';
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
@ -20,6 +28,31 @@ export function WidgetList({ params }: { params: Widget }) {
removeWidget(db, params.id);
};
const renderIcon = useCallback(
(kind: number) => {
switch (kind) {
case WidgetKinds.tmp.xfeed:
return <GroupFeedsIcon className="h-5 w-5 text-white" />;
case WidgetKinds.local.follows:
return <FollowsIcon className="h-5 w-5 text-white" />;
case WidgetKinds.local.files:
case WidgetKinds.global.files:
return <FileIcon className="h-5 w-5 text-white" />;
case WidgetKinds.local.articles:
case WidgetKinds.global.articles:
return <ArticleIcon className="h-5 w-5 text-white" />;
case WidgetKinds.tmp.xhashtag:
return <HashtagIcon className="h-5 w-4 text-white" />;
case WidgetKinds.nostrBand.trendingAccounts:
case WidgetKinds.nostrBand.trendingNotes:
return <TrendingIcon className="h-5 w-4 text-white" />;
default:
return '';
}
},
[DefaultWidgets]
);
const renderItem = useCallback(
(row: WidgetGroup) => {
return (
@ -27,10 +60,29 @@ export function WidgetList({ params }: { params: Widget }) {
<h3 className="font-medium text-white/50">{row.title}</h3>
<div className="flex flex-col divide-y divide-white/5 overflow-hidden rounded-xl bg-white/10">
{row.data.map((item, index) => (
<button onClick={() => openWidget(item)} key={index}>
<div className="inline-flex h-14 w-full flex-col items-start justify-center gap-1 px-4 hover:bg-white/10">
<h5 className="font-medium leading-none">{item.title}</h5>
<p className="text-xs leading-none text-white/50">{item.description}</p>
<button
onClick={() => openWidget(item)}
key={index}
className="flex items-center gap-2.5 px-4 hover:bg-white/10"
>
{item.icon ? (
<div className="h-10 w-10 shrink-0 rounded-md">
<img
src={item.icon}
alt={item.title}
className="h-10 w-10 object-cover"
/>
</div>
) : (
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-white/10">
{renderIcon(item.kind)}
</div>
)}
<div className="inline-flex h-16 w-full flex-col items-start justify-center gap-1">
<h5 className="line-clamp-1 font-medium leading-none">{item.title}</h5>
<p className="line-clamp-1 text-xs leading-none text-white/50">
{item.description}
</p>
</div>
</button>
))}
@ -44,7 +96,7 @@ export function WidgetList({ params }: { params: Widget }) {
return (
<div className="relative h-full shrink-0 grow-0 basis-[400px] overflow-hidden bg-white/10">
<TitleBar id={params.id} title="Add widget" />
<div className="flex flex-col gap-8 px-3">
<div className="flex flex-col gap-6 px-3">
{DefaultWidgets.map((row: WidgetGroup) => renderItem(row))}
</div>
<div className="mt-6 px-3">
@ -55,7 +107,7 @@ export function WidgetList({ params }: { params: Widget }) {
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-white/5 text-sm font-medium text-white/50"
>
Build your own widget{' '}
<div className="-rotate-3 transform rounded-md bg-white/10 px-1.5 py-1">
<div className="-rotate-3 transform rounded-md border border-white/20 bg-white/10 px-1.5 py-1">
<span className="bg-gradient-to-t from-fuchsia-200 via-red-200 to-orange-300 bg-clip-text text-xs text-transparent">
Coming soon
</span>

View File

@ -17,40 +17,32 @@ export const NDKInstance = () => {
const cacheAdapter = useMemo(() => new TauriAdapter(), [ndk]);
// TODO: fully support NIP-11
async function verifyRelays(relays: string[]) {
async function getExplicitRelays() {
try {
const urls: string[] = relays.map((relay) => {
if (relay.startsWith('ws')) {
return relay.replace('ws', 'http');
}
if (relay.startsWith('wss')) {
return relay.replace('wss', 'https');
}
});
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort('timeout'), 10000);
const requests = urls.map((url) =>
fetch(url, {
// get relays
const relays = (await db.getExplicitRelayUrls()) ?? FULL_RELAYS;
const requests = relays.map((relay) => {
const url = new URL(relay);
return fetch(`https://${url.hostname + url.pathname}`, {
headers: { Accept: 'application/nostr+json' },
signal: controller.signal,
})
);
});
});
const responses = await Promise.all(requests);
const errors = responses.filter((response) => !response.ok);
if (errors.length > 0) {
throw errors.map((response) => Error(response.statusText));
}
if (errors.length > 0) throw errors.map((response) => Error(response.statusText));
const verifiedRelays: string[] = responses.map((res) => {
if (res.url.startsWith('http')) {
return res.url.replace('htto', 'ws');
}
if (res.url.startsWith('https')) {
return res.url.replace('https', 'wss');
}
const url = new URL(res.url);
if (url.protocol === 'http:') return `ws://${url.hostname + url.pathname}`;
if (url.protocol === 'https:') return `wss://${url.hostname + url.pathname}`;
});
// clear timeout
@ -59,31 +51,14 @@ export const NDKInstance = () => {
// return all validate relays
return verifiedRelays;
} catch (e) {
console.error('verify relay failed with error: ', e);
await message(e, { title: 'Cannot connect to relays', type: 'error' });
}
}
async function initNDK() {
let explicitRelayUrls: string[];
const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls();
console.log('relays in db: ', explicitRelayUrlsFromDB);
console.log('ndk cache adapter: ', cacheAdapter);
if (explicitRelayUrlsFromDB) {
explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
} else {
explicitRelayUrls = await verifyRelays(FULL_RELAYS);
}
if (explicitRelayUrls.length < 1) {
await message('Something is wrong. No relays have been found.', {
title: 'Lume',
type: 'error',
});
}
const explicitRelayUrls = await getExplicitRelays();
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
try {
await instance.connect(10000);
} catch (error) {

View File

@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function ArticleIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M16.25 12V4.75a1 1 0 00-1-1H3.75a1 1 0 00-1 1v13a2.5 2.5 0 002.5 2.5H18.5M16.25 12v5.75a2.5 2.5 0 005 0V13a1 1 0 00-1-1h-4zm-9.5 3.75h5.5m-5.5-8h5.5v4.5h-5.5v-4.5z"
></path>
</svg>
);
}

View File

@ -13,10 +13,9 @@ export function FileIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M8.75 6.75h6.5m-6.5 4h6.5m-6.5 4h2.5m-5.5 6.5h12.5a1 1 0 001-1V3.75a1 1 0 00-1-1H5.75a1 1 0 00-1 1v16.5a1 1 0 001 1z"
/>
d="M12.75 3.25v5a1 1 0 001 1h5m-10 4h3.5m-3.5 4h6.5m-9.5-14.5h6.586a1 1 0 01.707.293l5.914 5.914a1 1 0 01.293.707V20.25a1 1 0 01-1 1H5.75a1 1 0 01-1-1V3.75a1 1 0 011-1z"
></path>
</svg>
);
}

View File

@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function FollowsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M17.75 19.25h3.673c.581 0 1.045-.496.947-1.07-.531-3.118-2.351-5.43-5.37-5.43-.446 0-.866.05-1.26.147M11.25 7a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm8.5.5a2.75 2.75 0 11-5.5 0 2.75 2.75 0 015.5 0zM1.87 19.18c.568-3.68 2.647-6.43 6.13-6.43 3.482 0 5.561 2.75 6.13 6.43.088.575-.375 1.07-.956 1.07H2.825c-.58 0-1.043-.495-.955-1.07z"
></path>
</svg>
);
}

View File

@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function GroupFeedsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M14.425 13.18c3.361-1.396 7.598.605 8.454 6.003.09.573-.372 1.067-.952 1.067H16.75M10.75 7a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm9 0a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm-6.966 13.25H2.072c-.58 0-1.042-.497-.951-1.07 1.362-8.573 11.252-8.573 12.614 0 .091.573-.371 1.07-.951 1.07z"
></path>
</svg>
);
}

View File

@ -16,7 +16,7 @@ export function HashtagIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElem
strokeLinejoin="round"
strokeWidth="1.5"
d="M8.75 3.75l-2 16.5m10.5-16.5l-2 16.5M3.75 7.75h16.5m0 8.5H3.75"
/>
></path>
</svg>
);
}

View File

@ -58,3 +58,6 @@ export * from './chevronUp';
export * from './secure';
export * from './verified';
export * from './mention';
export * from './groupFeeds';
export * from './article';
export * from './follows';

View File

@ -3,7 +3,7 @@ import { createJSONStorage, persist } from 'zustand/middleware';
import { LumeStorage } from '@libs/storage/instance';
import { Widget } from '@utils/types';
import { Widget, WidgetGroup } from '@utils/types';
interface WidgetState {
widgets: null | Array<Widget>;
@ -39,7 +39,7 @@ export const WidgetKinds = {
},
};
export const DefaultWidgets = [
export const DefaultWidgets: Array<WidgetGroup> = [
{
title: 'Network / Follows',
data: [

View File

@ -46,6 +46,7 @@ export interface WidgetGroupItem {
title: string;
description: string;
kind: number;
icon?: string;
}
export interface Widget {