mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
feat: follow and mute
This commit is contained in:
34
package-lock.json
generated
34
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@tanstack/query-persist-client-core": "^4.24.10",
|
"@tanstack/query-persist-client-core": "^4.24.10",
|
||||||
"@tanstack/query-sync-storage-persister": "^4.24.10",
|
"@tanstack/query-sync-storage-persister": "^4.24.10",
|
||||||
"@tanstack/solid-query": "^4.24.10",
|
"@tanstack/solid-query": "^4.24.10",
|
||||||
|
"@tanstack/solid-virtual": "^3.0.0-beta.6",
|
||||||
"@thisbeyond/solid-dnd": "^0.7.3",
|
"@thisbeyond/solid-dnd": "^0.7.3",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"heroicons": "^2.0.15",
|
"heroicons": "^2.0.15",
|
||||||
@@ -1166,6 +1167,11 @@
|
|||||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
|
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@reach/observe-rect": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ=="
|
||||||
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
@@ -1305,6 +1311,21 @@
|
|||||||
"solid-js": "^1.5.7"
|
"solid-js": "^1.5.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/solid-virtual": {
|
||||||
|
"version": "3.0.0-beta.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.0.0-beta.6.tgz",
|
||||||
|
"integrity": "sha512-/HjeHZb4UZxxFSAkICUEWOozGwHQpKlvtnUoS5uSMSuLOz0XM5vFq6zR6ENwAczKWDtkh8ntddk+zXAhyXOlEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@reach/observe-rect": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@thisbeyond/solid-dnd": {
|
"node_modules/@thisbeyond/solid-dnd": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.3.tgz",
|
||||||
@@ -8051,6 +8072,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@reach/observe-rect": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ=="
|
||||||
|
},
|
||||||
"@scure/base": {
|
"@scure/base": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
@@ -8138,6 +8164,14 @@
|
|||||||
"@tanstack/query-core": "4.24.10"
|
"@tanstack/query-core": "4.24.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tanstack/solid-virtual": {
|
||||||
|
"version": "3.0.0-beta.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.0.0-beta.6.tgz",
|
||||||
|
"integrity": "sha512-/HjeHZb4UZxxFSAkICUEWOozGwHQpKlvtnUoS5uSMSuLOz0XM5vFq6zR6ENwAczKWDtkh8ntddk+zXAhyXOlEw==",
|
||||||
|
"requires": {
|
||||||
|
"@reach/observe-rect": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@thisbeyond/solid-dnd": {
|
"@thisbeyond/solid-dnd": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.3.tgz",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"@tanstack/query-persist-client-core": "^4.24.10",
|
"@tanstack/query-persist-client-core": "^4.24.10",
|
||||||
"@tanstack/query-sync-storage-persister": "^4.24.10",
|
"@tanstack/query-sync-storage-persister": "^4.24.10",
|
||||||
"@tanstack/solid-query": "^4.24.10",
|
"@tanstack/solid-query": "^4.24.10",
|
||||||
|
"@tanstack/solid-virtual": "^3.0.0-beta.6",
|
||||||
"@thisbeyond/solid-dnd": "^0.7.3",
|
"@thisbeyond/solid-dnd": "^0.7.3",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"heroicons": "^2.0.15",
|
"heroicons": "^2.0.15",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createSignal, For, type JSX } from 'solid-js';
|
|||||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||||
|
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
|
import UserNameDisplay from './UserDisplayName';
|
||||||
|
|
||||||
type ConfigProps = {
|
type ConfigProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -21,7 +22,7 @@ const RelayConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="py-2">
|
||||||
<h3 class="font-bold">リレー</h3>
|
<h3 class="font-bold">リレー</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<For each={config().relayUrls}>
|
<For each={config().relayUrls}>
|
||||||
@@ -83,7 +84,7 @@ const DateFormatConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="py-2">
|
||||||
<h3 class="font-bold">時刻の表記</h3>
|
<h3 class="font-bold">時刻の表記</h3>
|
||||||
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
|
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
|
||||||
<For each={dateFormats}>
|
<For each={dateFormats}>
|
||||||
@@ -132,6 +133,68 @@ const ToggleButton = (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MuteConfig = () => {
|
||||||
|
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
|
||||||
|
|
||||||
|
const [keywordInput, setKeywordInput] = createSignal('');
|
||||||
|
|
||||||
|
const handleClickAddKeyword: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (keywordInput().length === 0) return;
|
||||||
|
addMutedKeyword(keywordInput());
|
||||||
|
setKeywordInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="py-2">
|
||||||
|
<h3 class="font-bold">ミュートしたユーザ</h3>
|
||||||
|
<ul class="flex flex-col">
|
||||||
|
<For each={config().mutedPubkeys}>
|
||||||
|
{(pubkey) => (
|
||||||
|
<li class="flex items-center">
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<UserNameDisplay pubkey={pubkey} />
|
||||||
|
</div>
|
||||||
|
<button class="h-3 w-3 shrink-0" onClick={() => removeMutedPubkey(pubkey)}>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="py-2">
|
||||||
|
<h3 class="font-bold">ミュートした単語</h3>
|
||||||
|
<ul class="flex flex-col">
|
||||||
|
<For each={config().mutedKeywords}>
|
||||||
|
{(keyword) => (
|
||||||
|
<li class="flex items-center">
|
||||||
|
<div class="flex-1 truncate">{keyword}</div>
|
||||||
|
<button class="h-3 w-3 shrink-0" onClick={() => removeMutedKeyword(keyword)}>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
<form class="flex gap-2" onSubmit={handleClickAddKeyword}>
|
||||||
|
<input
|
||||||
|
class="flex-1"
|
||||||
|
type="text"
|
||||||
|
name="keyword"
|
||||||
|
value={keywordInput()}
|
||||||
|
onChange={(ev) => setKeywordInput(ev.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
|
追加
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const OtherConfig = () => {
|
const OtherConfig = () => {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
|
|
||||||
@@ -150,7 +213,7 @@ const OtherConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="py-2">
|
||||||
<h3 class="font-bold">その他</h3>
|
<h3 class="font-bold">その他</h3>
|
||||||
<div class="flex flex-col justify-evenly gap-2">
|
<div class="flex flex-col justify-evenly gap-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
@@ -194,6 +257,7 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
<RelayConfig />
|
<RelayConfig />
|
||||||
<DateFormatConfig />
|
<DateFormatConfig />
|
||||||
<OtherConfig />
|
<OtherConfig />
|
||||||
|
<MuteConfig />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export type ContextMenuProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type MenuDisplayProps = {
|
export type MenuDisplayProps = {
|
||||||
|
menuRef: (elem: HTMLUListElement) => void;
|
||||||
menu: MenuItem[];
|
menu: MenuItem[];
|
||||||
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +39,11 @@ const MenuItemDisplay: Component<MenuItemDisplayProps> = (props) => {
|
|||||||
|
|
||||||
const MenuDisplay: Component<MenuDisplayProps> = (props) => {
|
const MenuDisplay: Component<MenuDisplayProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul
|
||||||
|
ref={props.menuRef}
|
||||||
|
class="absolute z-20 min-w-[48px] rounded border bg-white shadow-md"
|
||||||
|
classList={{ hidden: !props.isOpen, block: props.isOpen }}
|
||||||
|
>
|
||||||
<For each={props.menu}>
|
<For each={props.menu}>
|
||||||
{(item) => <MenuItemDisplay item={item} onClose={props.onClose} />}
|
{(item) => <MenuItemDisplay item={item} onClose={props.onClose} />}
|
||||||
</For>
|
</For>
|
||||||
@@ -46,7 +52,7 @@ const MenuDisplay: Component<MenuDisplayProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenu: Component<ContextMenuProps> = (props) => {
|
const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||||
let menuRef: HTMLDivElement | undefined;
|
let menuRef: HTMLUListElement | undefined;
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
|
||||||
@@ -66,8 +72,9 @@ const ContextMenu: Component<ContextMenuProps> = (props) => {
|
|||||||
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||||
if (menuRef == null) return;
|
if (menuRef == null) return;
|
||||||
|
|
||||||
const buttonRect = ev.target.getBoundingClientRect();
|
const buttonRect = ev.currentTarget.getBoundingClientRect();
|
||||||
menuRef.style.left = `${buttonRect.left}px`;
|
const menuRect = menuRef.getBoundingClientRect();
|
||||||
|
menuRef.style.left = `${buttonRect.left - buttonRect.width}px`;
|
||||||
menuRef.style.top = `${buttonRect.top + buttonRect.height}px`;
|
menuRef.style.top = `${buttonRect.top + buttonRect.height}px`;
|
||||||
|
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
@@ -86,13 +93,14 @@ const ContextMenu: Component<ContextMenuProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={handleClick}>{props.children}</button>
|
<button onClick={handleClick}>{props.children}</button>
|
||||||
<div
|
<MenuDisplay
|
||||||
ref={menuRef}
|
menuRef={(e) => {
|
||||||
class="absolute z-20 min-w-[48px] rounded border bg-white shadow-md"
|
menuRef = e;
|
||||||
classList={{ hidden: !isOpen(), block: isOpen() }}
|
}}
|
||||||
>
|
menu={props.menu}
|
||||||
<MenuDisplay menu={props.menu} onClose={() => setIsOpen(false)} />
|
isOpen={isOpen()}
|
||||||
</div>
|
onClose={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Component, createSignal, createMemo, Show, Switch, Match, createEffect } from 'solid-js';
|
import { Component, createSignal, createMemo, Show, Switch, Match, createEffect } from 'solid-js';
|
||||||
|
import { createMutation } from '@tanstack/solid-query';
|
||||||
|
|
||||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||||
import CheckCircle from 'heroicons/24/solid/check-circle.svg';
|
import CheckCircle from 'heroicons/24/solid/check-circle.svg';
|
||||||
import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg';
|
import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg';
|
||||||
import ArrowPath from 'heroicons/24/outline/arrow-path.svg';
|
import ArrowPath from 'heroicons/24/outline/arrow-path.svg';
|
||||||
|
import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg';
|
||||||
|
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import Timeline from '@/components/Timeline';
|
import Timeline from '@/components/Timeline';
|
||||||
@@ -17,11 +19,14 @@ import useVerification from '@/nostr/useVerification';
|
|||||||
import useFollowings from '@/nostr/useFollowings';
|
import useFollowings from '@/nostr/useFollowings';
|
||||||
import useFollowers from '@/nostr/useFollowers';
|
import useFollowers from '@/nostr/useFollowers';
|
||||||
import useConfig from '@/nostr/useConfig';
|
import useConfig from '@/nostr/useConfig';
|
||||||
|
import useCommands from '@/nostr/useCommands';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
|
||||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
import epoch from '@/utils/epoch';
|
import epoch from '@/utils/epoch';
|
||||||
|
import timeout from '@/utils/timeout';
|
||||||
|
import ContextMenu, { MenuItem } from './ContextMenu';
|
||||||
|
|
||||||
export type ProfileDisplayProps = {
|
export type ProfileDisplayProps = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@@ -37,8 +42,11 @@ const FollowersCount: Component<{ pubkey: string }> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||||
const { config } = useConfig();
|
const { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted } = useConfig();
|
||||||
const pubkey = usePubkey();
|
const commands = useCommands();
|
||||||
|
const myPubkey = usePubkey();
|
||||||
|
|
||||||
|
const npub = createMemo(() => npubEncodeFallback(props.pubkey));
|
||||||
|
|
||||||
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
||||||
const [showFollowers, setShowFollowers] = createSignal(false);
|
const [showFollowers, setShowFollowers] = createSignal(false);
|
||||||
@@ -58,25 +66,103 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
return { user, domain, ident };
|
return { user, domain, ident };
|
||||||
};
|
};
|
||||||
const isVerified = () => verification()?.pubkey === props.pubkey;
|
const isVerified = () => verification()?.pubkey === props.pubkey;
|
||||||
|
const isMuted = () => isPubkeyMuted(props.pubkey);
|
||||||
|
|
||||||
const { followingPubkeys: myFollowingPubkeys } = useFollowings(() =>
|
const {
|
||||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
|
followingPubkeys: myFollowingPubkeys,
|
||||||
|
invalidateFollowings: invalidateMyFollowings,
|
||||||
|
query: myFollowingQuery,
|
||||||
|
} = useFollowings(() =>
|
||||||
|
ensureNonNull([myPubkey()] as const)(([pubkeyNonNull]) => ({
|
||||||
pubkey: pubkeyNonNull,
|
pubkey: pubkeyNonNull,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const following = () => myFollowingPubkeys().includes(props.pubkey);
|
const following = () => myFollowingPubkeys().includes(props.pubkey);
|
||||||
|
|
||||||
const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings(
|
const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings(
|
||||||
() => ({
|
() => ({ pubkey: props.pubkey }),
|
||||||
pubkey: props.pubkey,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const followed = () => {
|
const followed = () => {
|
||||||
const p = pubkey();
|
const p = myPubkey();
|
||||||
return p != null && userFollowingPubkeys().includes(p);
|
return p != null && userFollowingPubkeys().includes(p);
|
||||||
};
|
};
|
||||||
|
|
||||||
const npub = createMemo(() => npubEncodeFallback(props.pubkey));
|
const updateContactsMutation = createMutation({
|
||||||
|
mutationKey: ['updateContacts'],
|
||||||
|
mutationFn: (...params: Parameters<typeof commands.updateContacts>) =>
|
||||||
|
commands
|
||||||
|
.updateContacts(...params)
|
||||||
|
.then((promises) => Promise.allSettled(promises.map(timeout(5000)))),
|
||||||
|
onSuccess: (results) => {
|
||||||
|
const succeeded = results.filter((res) => res.status === 'fulfilled').length;
|
||||||
|
const failed = results.length - succeeded;
|
||||||
|
if (succeeded === results.length) {
|
||||||
|
console.log('succeeded to update contacts');
|
||||||
|
} else if (succeeded > 0) {
|
||||||
|
console.log(
|
||||||
|
`succeeded to update contacts for ${succeeded} relays but failed for ${failed} relays`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('failed to update contacts');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error('failed to update contacts: ', err);
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
invalidateMyFollowings()
|
||||||
|
.then(() => myFollowingQuery.refetch())
|
||||||
|
.catch((err) => console.error('failed to refetch contacts', err));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const follow = () => {
|
||||||
|
const p = myPubkey();
|
||||||
|
if (p == null) return;
|
||||||
|
if (!myFollowingQuery.isFetched) return;
|
||||||
|
|
||||||
|
updateContactsMutation.mutate({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
pubkey: p,
|
||||||
|
content: myFollowingQuery.data?.content ?? '',
|
||||||
|
followingPubkeys: [...myFollowingPubkeys(), props.pubkey],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfollow = () => {
|
||||||
|
const p = myPubkey();
|
||||||
|
if (p == null) return;
|
||||||
|
if (!myFollowingQuery.isFetched) return;
|
||||||
|
|
||||||
|
if (!window.confirm('本当にフォロー解除しますか?')) return;
|
||||||
|
|
||||||
|
updateContactsMutation.mutate({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
pubkey: p,
|
||||||
|
content: myFollowingQuery.data?.content ?? '',
|
||||||
|
followingPubkeys: myFollowingPubkeys().filter((k) => k !== props.pubkey),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu: MenuItem[] = [
|
||||||
|
{
|
||||||
|
content: () => 'IDをコピー',
|
||||||
|
onSelect: () => {
|
||||||
|
navigator.clipboard.writeText(npub()).catch((err) => window.alert(err));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: () => (!isMuted() ? 'ミュート' : 'ミュート解除'),
|
||||||
|
onSelect: () => {
|
||||||
|
if (!isMuted()) {
|
||||||
|
addMutedPubkey(props.pubkey);
|
||||||
|
} else {
|
||||||
|
removeMutedPubkey(props.pubkey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const { events } = useSubscription(() => ({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
@@ -113,100 +199,118 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<div class="mt-[-54px] flex items-end gap-4 px-4 pt-4">
|
<div class="mt-[-54px] flex items-end gap-4 px-4 pt-4">
|
||||||
<div class="h-28 w-28 shrink-0 rounded-lg shadow-md">
|
<div class="flex-1 shrink-0">
|
||||||
<Show when={profile()?.picture} keyed>
|
<div class="h-28 w-28 rounded-lg shadow-md">
|
||||||
{(pictureUrl) => (
|
<Show when={profile()?.picture} keyed>
|
||||||
<img
|
{(pictureUrl) => (
|
||||||
src={pictureUrl}
|
<img
|
||||||
alt="user icon"
|
src={pictureUrl}
|
||||||
class="h-full w-full rounded-lg object-cover"
|
alt="user icon"
|
||||||
/>
|
class="h-full w-full rounded-lg object-cover"
|
||||||
)}
|
/>
|
||||||
</Show>
|
)}
|
||||||
</div>
|
|
||||||
<div class="flex items-start overflow-hidden">
|
|
||||||
<div class="h-16 shrink overflow-hidden">
|
|
||||||
<Show when={(profile()?.display_name?.length ?? 0) > 0}>
|
|
||||||
<div class="truncate text-xl font-bold">{profile()?.display_name}</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Show when={(profile()?.name?.length ?? 0) > 0}>
|
|
||||||
<div class="truncate text-xs">@{profile()?.name}</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={(profile()?.nip05?.length ?? 0) > 0}>
|
|
||||||
<div class="flex items-center text-xs">
|
|
||||||
{nip05Identifier()?.ident}
|
|
||||||
<Switch
|
|
||||||
fallback={
|
|
||||||
<span class="inline-block h-4 w-4 text-rose-500">
|
|
||||||
<ExclamationCircle />
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Match when={verificationQuery.isLoading}>
|
|
||||||
<span class="inline-block h-3 w-3">
|
|
||||||
<ArrowPath />
|
|
||||||
</span>
|
|
||||||
</Match>
|
|
||||||
<Match when={isVerified()}>
|
|
||||||
<span class="inline-block h-4 w-4 text-blue-400">
|
|
||||||
<CheckCircle />
|
|
||||||
</span>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<div class="truncate text-xs">{npub()}</div>
|
|
||||||
<Copy
|
|
||||||
class="h-4 w-4 shrink-0 text-stone-500 hover:text-stone-700"
|
|
||||||
text={npub()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 flex-col items-center justify-center gap-1">
|
</div>
|
||||||
{/*
|
<div class="flex shrink-0 flex-col items-center gap-1">
|
||||||
<Switch
|
<div class="flex flex-row justify-start gap-1">
|
||||||
fallback={
|
<Switch>
|
||||||
<button
|
<Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}>
|
||||||
class="w-24 rounded-full border border-primary px-4 py-2 text-primary
|
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
||||||
hover:border-rose-400 hover:text-rose-400"
|
読み込み中
|
||||||
>
|
</span>
|
||||||
フォロー
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Match when={props.pubkey === pubkey()}>
|
|
||||||
<button
|
|
||||||
class="w-20 rounded-full border border-primary px-4 py-2 text-primary
|
|
||||||
hover:border-rose-400 hover:text-rose-400"
|
|
||||||
>
|
|
||||||
編集
|
|
||||||
</button>
|
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={updateContactsMutation.isLoading}>
|
||||||
|
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
||||||
|
更新中
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
{/*
|
||||||
|
<Match when={props.pubkey === myPubkey()}>
|
||||||
|
<span class="rounded-full border border-primary px-4 py-2 text-primary">
|
||||||
|
あなたです
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
*/}
|
||||||
<Match when={following()}>
|
<Match when={following()}>
|
||||||
<button
|
<button
|
||||||
class="w-32 rounded-full border border-primary bg-primary px-4 py-2
|
class="rounded-full border border-primary bg-primary px-4 py-2
|
||||||
text-center font-bold text-white hover:bg-rose-500"
|
text-center font-bold text-white hover:bg-rose-500 sm:w-32"
|
||||||
onMouseEnter={() => setHoverFollowButton(true)}
|
onMouseEnter={() => setHoverFollowButton(true)}
|
||||||
onMouseLeave={() => setHoverFollowButton(false)}
|
onMouseLeave={() => setHoverFollowButton(false)}
|
||||||
|
onClick={() => unfollow()}
|
||||||
|
disabled={updateContactsMutation.isLoading}
|
||||||
>
|
>
|
||||||
<Show when={!hoverFollowButton()} fallback="フォロー解除">
|
<Show when={!hoverFollowButton()} fallback="フォロー解除">
|
||||||
フォロー中
|
フォロー中
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={!following()}>
|
||||||
|
<button
|
||||||
|
class="w-24 rounded-full border border-primary px-4 py-2 text-primary
|
||||||
|
hover:border-rose-400 hover:text-rose-400"
|
||||||
|
onClick={() => follow()}
|
||||||
|
disabled={updateContactsMutation.isLoading}
|
||||||
|
>
|
||||||
|
フォロー
|
||||||
|
</button>
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
*/}
|
<ContextMenu menu={menu}>
|
||||||
<Show when={followed()}>
|
<button
|
||||||
<div class="shrink-0 text-xs">フォローされています</div>
|
class="w-10 rounded-full border border-primary p-2 text-primary
|
||||||
|
hover:border-rose-400 hover:text-rose-400"
|
||||||
|
>
|
||||||
|
<EllipsisHorizontal />
|
||||||
|
</button>
|
||||||
|
</ContextMenu>
|
||||||
|
</div>
|
||||||
|
<Show when={followed()}>
|
||||||
|
<div class="shrink-0 text-xs">フォローされています</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start px-4 pt-2">
|
||||||
|
<div class="h-16 shrink overflow-hidden">
|
||||||
|
<Show when={(profile()?.display_name?.length ?? 0) > 0}>
|
||||||
|
<div class="truncate text-xl font-bold">{profile()?.display_name}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Show when={(profile()?.name?.length ?? 0) > 0}>
|
||||||
|
<div class="truncate text-xs">@{profile()?.name}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={(profile()?.nip05?.length ?? 0) > 0}>
|
||||||
|
<div class="flex items-center text-xs">
|
||||||
|
{nip05Identifier()?.ident}
|
||||||
|
<Switch
|
||||||
|
fallback={
|
||||||
|
<span class="inline-block h-4 w-4 text-rose-500">
|
||||||
|
<ExclamationCircle />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Match when={verificationQuery.isLoading}>
|
||||||
|
<span class="inline-block h-3 w-3">
|
||||||
|
<ArrowPath />
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
<Match when={isVerified()}>
|
||||||
|
<span class="inline-block h-4 w-4 text-blue-400">
|
||||||
|
<CheckCircle />
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div class="truncate text-xs">{npub()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={(profile()?.about ?? '').length > 0}>
|
<Show when={(profile()?.about ?? '').length > 0}>
|
||||||
<div class="max-h-40 shrink-0 overflow-y-auto whitespace-pre-wrap px-5 py-3 text-sm">
|
<div class="max-h-40 shrink-0 overflow-y-auto whitespace-pre-wrap px-4 py-2 text-sm">
|
||||||
{profile()?.about}
|
{profile()?.about}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { type Component } from 'solid-js';
|
import { Show, type Component } from 'solid-js';
|
||||||
|
|
||||||
import ColumnItem from '@/components/ColumnItem';
|
import ColumnItem from '@/components/ColumnItem';
|
||||||
|
import useConfig from '@/nostr/useConfig';
|
||||||
import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay';
|
import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay';
|
||||||
|
|
||||||
export type TextNoteProps = TextNoteDisplayProps;
|
export type TextNoteProps = TextNoteDisplayProps;
|
||||||
|
|
||||||
const TextNote: Component<TextNoteProps> = (props) => {
|
const TextNote: Component<TextNoteProps> = (props) => {
|
||||||
|
const { shouldMuteEvent } = useConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnItem>
|
<Show when={!shouldMuteEvent(props.event)}>
|
||||||
<TextNoteDisplay {...props} />
|
<ColumnItem>
|
||||||
</ColumnItem>
|
<TextNoteDisplay {...props} />
|
||||||
|
</ColumnItem>
|
||||||
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
{
|
{
|
||||||
content: () => 'JSONとしてコピー',
|
content: () => 'JSONとしてコピー',
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
navigator.clipboard.writeText(JSON.stringify(props.event)).catch((err) => window.alert(err));
|
navigator.clipboard
|
||||||
|
.writeText(JSON.stringify(props.event))
|
||||||
|
.catch((err) => window.alert(err));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -94,11 +96,13 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
mutationFn: commands.publishReaction.bind(commands),
|
mutationFn: commands.publishReaction.bind(commands),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log('succeeded to publish reaction');
|
console.log('succeeded to publish reaction');
|
||||||
invalidateReactions().catch((err) => console.error('failed to refetch reactions', err));
|
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('failed to publish reaction: ', err);
|
console.error('failed to publish reaction: ', err);
|
||||||
},
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
invalidateReactions().catch((err) => console.error('failed to refetch reactions', err));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const publishDeprecatedRepostMutation = createMutation({
|
const publishDeprecatedRepostMutation = createMutation({
|
||||||
@@ -106,11 +110,13 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
mutationFn: commands.publishDeprecatedRepost.bind(commands),
|
mutationFn: commands.publishDeprecatedRepost.bind(commands),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log('succeeded to publish reposts');
|
console.log('succeeded to publish reposts');
|
||||||
invalidateDeprecatedReposts().catch((err) => console.error('failed to refetch reposts', err));
|
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error('failed to publish repost: ', err);
|
console.error('failed to publish repost: ', err);
|
||||||
},
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
invalidateDeprecatedReposts().catch((err) => console.error('failed to refetch reposts', err));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isReactedByMe = createMemo(() => {
|
const isReactedByMe = createMemo(() => {
|
||||||
|
|||||||
@@ -14,14 +14,21 @@ type TextNoteDisplayByIdProps = Omit<TextNoteDisplayProps, 'event'> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
|
const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
|
||||||
|
const { shouldMuteEvent } = useConfig();
|
||||||
const { event, query: eventQuery } = useEvent(() =>
|
const { event, query: eventQuery } = useEvent(() =>
|
||||||
ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({
|
ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({
|
||||||
eventId: eventIdNonNull,
|
eventId: eventIdNonNull,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hidden = (): boolean => {
|
||||||
|
const ev = event();
|
||||||
|
return ev != null && shouldMuteEvent(ev);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch fallback="投稿が見つかりません">
|
<Switch fallback="投稿が見つかりません">
|
||||||
|
<Match when={hidden()}>{null}</Match>
|
||||||
<Match when={event()} keyed>
|
<Match when={event()} keyed>
|
||||||
{(ev) => <TextNoteDisplay event={ev} {...props} />}
|
{(ev) => <TextNoteDisplay event={ev} {...props} />}
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// import { z } from 'zod';
|
// import { z } from 'zod';
|
||||||
import { type Event as NostrEvent, type Filter } from 'nostr-tools';
|
import { type Filter } from 'nostr-tools';
|
||||||
import { type ColumnProps } from '@/components/Column';
|
import { type ColumnProps } from '@/components/Column';
|
||||||
|
|
||||||
export type NotificationType =
|
export type NotificationType =
|
||||||
@@ -43,42 +43,42 @@ type BulidOptions = {
|
|||||||
|
|
||||||
export type BaseColumn = {
|
export type BaseColumn = {
|
||||||
title: string;
|
title: string;
|
||||||
columnWidth: ColumnProps['width'];
|
width: ColumnProps['width'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows posts by following users */
|
/** A column which shows posts by following users */
|
||||||
export type FollowingColumn = {
|
export type FollowingColumn = BaseColumn & {
|
||||||
columnType: 'Following';
|
columnType: 'Following';
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows replies, reactions, reposts to the specific user */
|
/** A column which shows replies, reactions, reposts to the specific user */
|
||||||
export type NotificationColumn = {
|
export type NotificationColumn = BaseColumn & {
|
||||||
columnType: 'Notification';
|
columnType: 'Notification';
|
||||||
notificationTypes: NotificationType[];
|
// notificationTypes: NotificationType[];
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows posts from the specific user */
|
/** A column which shows posts from the specific user */
|
||||||
export type PostsColumn = {
|
export type PostsColumn = BaseColumn & {
|
||||||
columnType: 'Posts';
|
columnType: 'Posts';
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows reactions published by the specific user */
|
/** A column which shows reactions published by the specific user */
|
||||||
export type ReactionsColumn = {
|
export type ReactionsColumn = BaseColumn & {
|
||||||
columnType: 'Reactions';
|
columnType: 'Reactions';
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows text notes and reposts posted to the specific relays */
|
/** A column which shows text notes and reposts posted to the specific relays */
|
||||||
export type GlobalColumn = {
|
export type GlobalColumn = BaseColumn & {
|
||||||
columnType: 'Global';
|
columnType: 'Global';
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A column which shows text notes and reposts posted to the specific relays */
|
/** A column which shows text notes and reposts posted to the specific relays */
|
||||||
export type CustomFilterColumn = {
|
export type CustomFilterColumn = BaseColumn & {
|
||||||
columnType: 'CustomFilter';
|
columnType: 'CustomFilter';
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ type Following = {
|
|||||||
export type UseFollowings = {
|
export type UseFollowings = {
|
||||||
followings: () => Following[];
|
followings: () => Following[];
|
||||||
followingPubkeys: () => string[];
|
followingPubkeys: () => string[];
|
||||||
|
invalidateFollowings: () => Promise<void>;
|
||||||
query: CreateQueryResult<NostrEvent | null>;
|
query: CreateQueryResult<NostrEvent | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,7 +206,7 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { config } = useConfig();
|
const { config, shouldMuteEvent } = useConfig();
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
|
|
||||||
const sub = pool().sub(config().relayUrls, filters, {});
|
const sub = pool().sub(config().relayUrls, filters, {});
|
||||||
@@ -213,12 +214,15 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
|||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
sub.on('event', (event: NostrEvent & { id: string }) => {
|
sub.on('event', (event: NostrEvent & { id: string }) => {
|
||||||
if (config().mutedPubkeys.includes(event.id)) return;
|
|
||||||
if (event.kind === Kind.Metadata) {
|
if (event.kind === Kind.Metadata) {
|
||||||
const registeredTasks = profileTasks.get(event.pubkey) ?? [];
|
const registeredTasks = profileTasks.get(event.pubkey) ?? [];
|
||||||
resolveTasks(registeredTasks, event);
|
resolveTasks(registeredTasks, event);
|
||||||
} else if (event.kind === Kind.Text) {
|
return;
|
||||||
if (config().mutedPubkeys.includes(event.pubkey)) return;
|
}
|
||||||
|
|
||||||
|
if (shouldMuteEvent(event)) return;
|
||||||
|
|
||||||
|
if (event.kind === Kind.Text) {
|
||||||
const registeredTasks = textNoteTasks.get(event.id) ?? [];
|
const registeredTasks = textNoteTasks.get(event.id) ?? [];
|
||||||
resolveTasks(registeredTasks, event);
|
resolveTasks(registeredTasks, event);
|
||||||
} else if (event.kind === Kind.Reaction) {
|
} else if (event.kind === Kind.Reaction) {
|
||||||
@@ -229,7 +233,6 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
|||||||
resolveTasks(registeredTasks, event);
|
resolveTasks(registeredTasks, event);
|
||||||
});
|
});
|
||||||
} else if ((event.kind as number) === 6) {
|
} else if ((event.kind as number) === 6) {
|
||||||
if (config().mutedPubkeys.includes(event.pubkey)) return;
|
|
||||||
const eventTags = eventWrapper(event).taggedEvents();
|
const eventTags = eventWrapper(event).taggedEvents();
|
||||||
eventTags.forEach((eventTag) => {
|
eventTags.forEach((eventTag) => {
|
||||||
const taggedEventId = eventTag.id;
|
const taggedEventId = eventTag.id;
|
||||||
@@ -494,5 +497,7 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U
|
|||||||
|
|
||||||
const followingPubkeys = (): string[] => followings().map((follow) => follow.pubkey);
|
const followingPubkeys = (): string[] => followings().map((follow) => follow.pubkey);
|
||||||
|
|
||||||
return { followings, followingPubkeys, query };
|
const invalidateFollowings = (): Promise<void> => queryClient.invalidateQueries(genQueryKey());
|
||||||
|
|
||||||
|
return { followings, followingPubkeys, invalidateFollowings, query };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const useCommands = () => {
|
|||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
},
|
},
|
||||||
// NIP-18
|
// NIP-18
|
||||||
publishDeprecatedRepost({
|
async publishDeprecatedRepost({
|
||||||
relayUrls,
|
relayUrls,
|
||||||
pubkey,
|
pubkey,
|
||||||
eventId,
|
eventId,
|
||||||
@@ -171,8 +171,6 @@ const useCommands = () => {
|
|||||||
};
|
};
|
||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
},
|
},
|
||||||
// useFollowingsのisFetchedが呼ばれたとしても全てのリレーから取得できたとは限らない
|
|
||||||
// 半数以上、あるいは5秒待ってみて応答があればそれを利用するみたいな仕組みが必要か?
|
|
||||||
updateContacts({
|
updateContacts({
|
||||||
relayUrls,
|
relayUrls,
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -194,7 +192,6 @@ const useCommands = () => {
|
|||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
// TODO publishできたら、invalidateをしないといけない
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { type Accessor, type Setter } from 'solid-js';
|
import { type Accessor, type Setter } from 'solid-js';
|
||||||
|
import { Kind, type Event as NostrEvent } from 'nostr-tools';
|
||||||
import {
|
import {
|
||||||
createStorageWithSerializer,
|
createStorageWithSerializer,
|
||||||
createStoreWithStorage,
|
createStoreWithStorage,
|
||||||
} from '@/hooks/createSignalWithStorage';
|
} from '@/hooks/createSignalWithStorage';
|
||||||
|
import { ColumnConfig } from '@/core/column';
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
|
columns: ColumnConfig[];
|
||||||
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
||||||
keepOpenPostForm: boolean;
|
keepOpenPostForm: boolean;
|
||||||
showImage: boolean;
|
showImage: boolean;
|
||||||
mutedPubkeys: string[];
|
mutedPubkeys: string[];
|
||||||
|
mutedKeywords: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseConfig = {
|
type UseConfig = {
|
||||||
@@ -19,31 +23,46 @@ type UseConfig = {
|
|||||||
removeRelay: (url: string) => void;
|
removeRelay: (url: string) => void;
|
||||||
addMutedPubkey: (pubkey: string) => void;
|
addMutedPubkey: (pubkey: string) => void;
|
||||||
removeMutedPubkey: (pubkey: string) => void;
|
removeMutedPubkey: (pubkey: string) => void;
|
||||||
|
addMutedKeyword: (keyword: string) => void;
|
||||||
|
removeMutedKeyword: (keyword: string) => void;
|
||||||
|
isPubkeyMuted: (pubkey: string) => boolean;
|
||||||
|
shouldMuteEvent: (event: NostrEvent) => boolean;
|
||||||
|
initializeColumns: (param: { pubkey: string }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const relaysGlobal = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.snort.social',
|
||||||
|
'wss://relay.current.fyi',
|
||||||
|
];
|
||||||
|
|
||||||
|
const relaysOnlyAvailableInJP = [
|
||||||
|
'wss://relay-jp.nostr.wirednet.jp',
|
||||||
|
'wss://nostr.h3z.jp',
|
||||||
|
'wss://nostr.holybea.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
const relaysInJP = [
|
||||||
|
...relaysOnlyAvailableInJP,
|
||||||
|
'wss://nostr.holybea.com',
|
||||||
|
'wss://nostr-relay.nokotaro.com',
|
||||||
|
];
|
||||||
|
|
||||||
const InitialConfig = (): Config => {
|
const InitialConfig = (): Config => {
|
||||||
const relayUrls = [
|
const relayUrls = [...relaysGlobal];
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
|
||||||
'wss://relay.snort.social',
|
|
||||||
'wss://relay.current.fyi',
|
|
||||||
];
|
|
||||||
if (navigator.language === 'ja') {
|
if (navigator.language === 'ja') {
|
||||||
relayUrls.push(
|
relayUrls.push(...relaysInJP);
|
||||||
'wss://nostr.h3z.jp',
|
|
||||||
'wss://relay.nostr.wirednet.jp',
|
|
||||||
'wss://relay-jp.nostr.wirednet.jp',
|
|
||||||
'wss://nostr.holybea.com',
|
|
||||||
'wss://nostr-relay.nokotaro.com',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
relayUrls,
|
relayUrls,
|
||||||
|
columns: [],
|
||||||
dateFormat: 'relative',
|
dateFormat: 'relative',
|
||||||
keepOpenPostForm: false,
|
keepOpenPostForm: false,
|
||||||
showImage: true,
|
showImage: true,
|
||||||
mutedPubkeys: [],
|
mutedPubkeys: [],
|
||||||
|
mutedKeywords: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,6 +94,40 @@ const useConfig = (): UseConfig => {
|
|||||||
setConfig('mutedPubkeys', (current) => current.filter((e) => e !== pubkey));
|
setConfig('mutedPubkeys', (current) => current.filter((e) => e !== pubkey));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addMutedKeyword = (keyword: string) => {
|
||||||
|
setConfig('mutedKeywords', (current) => [...current, keyword]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMutedKeyword = (keyword: string) => {
|
||||||
|
setConfig('mutedKeywords', (current) => current.filter((e) => e !== keyword));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPubkeyMuted = (pubkey: string) => config.mutedPubkeys.includes(pubkey);
|
||||||
|
|
||||||
|
const hasMutedKeyword = (event: NostrEvent) => {
|
||||||
|
if (event.kind === Kind.Text) {
|
||||||
|
return config.mutedKeywords.some((keyword) => event.content.includes(keyword));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldMuteEvent = (event: NostrEvent) => isPubkeyMuted(event.pubkey) || hasMutedKeyword(event);
|
||||||
|
|
||||||
|
const initializeColumns = ({ pubkey }: { pubkey: string }) => {
|
||||||
|
// すでに設定されている場合は終了
|
||||||
|
if ((config.columns?.length ?? 0) > 0) return;
|
||||||
|
|
||||||
|
const myColumns: ColumnConfig[] = [
|
||||||
|
{ columnType: 'Following', title: 'ホーム', width: 'widest', pubkey },
|
||||||
|
{ columnType: 'Notification', title: '通知', width: 'medium', pubkey },
|
||||||
|
{ columnType: 'Posts', title: '自分の投稿', width: 'medium', pubkey },
|
||||||
|
{ columnType: 'Reactions', title: '自分のリアクション', width: 'medium', pubkey },
|
||||||
|
// { columnType: 'Global', relays: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
setConfig('columns', () => [...myColumns]);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: () => config,
|
config: () => config,
|
||||||
setConfig,
|
setConfig,
|
||||||
@@ -82,6 +135,11 @@ const useConfig = (): UseConfig => {
|
|||||||
removeRelay,
|
removeRelay,
|
||||||
addMutedPubkey,
|
addMutedPubkey,
|
||||||
removeMutedPubkey,
|
removeMutedPubkey,
|
||||||
|
addMutedKeyword,
|
||||||
|
removeMutedKeyword,
|
||||||
|
isPubkeyMuted,
|
||||||
|
shouldMuteEvent,
|
||||||
|
initializeColumns,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, createEffect, onCleanup } from 'solid-js';
|
import { createSignal, createEffect, onCleanup, on } from 'solid-js';
|
||||||
import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools';
|
import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import usePool from '@/nostr/usePool';
|
import usePool from '@/nostr/usePool';
|
||||||
@@ -35,10 +35,20 @@ setInterval(() => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||||
const { config } = useConfig();
|
const { config, shouldMuteEvent } = useConfig();
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
const [events, setEvents] = createSignal<NostrEvent[]>([]);
|
const [events, setEvents] = createSignal<NostrEvent[]>([]);
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => [config().mutedPubkeys, config().mutedKeywords],
|
||||||
|
() => {
|
||||||
|
setEvents((currentEvents) => currentEvents.filter((event) => !shouldMuteEvent(event)));
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const startSubscription = () => {
|
const startSubscription = () => {
|
||||||
const props = propsProvider();
|
const props = propsProvider();
|
||||||
if (props == null) return;
|
if (props == null) return;
|
||||||
@@ -58,7 +68,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
if (onEvent != null) {
|
if (onEvent != null) {
|
||||||
onEvent(event as NostrEvent & { id: string });
|
onEvent(event as NostrEvent & { id: string });
|
||||||
}
|
}
|
||||||
if (config().mutedPubkeys.includes(event.pubkey)) {
|
if (shouldMuteEvent(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props.clientEventFilter != null && !props.clientEventFilter(event)) {
|
if (props.clientEventFilter != null && !props.clientEventFilter(event)) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
type Component,
|
type Component,
|
||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
|
import { createVirtualizer } from '@tanstack/solid-virtual';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
|
|
||||||
import Column from '@/components/Column';
|
import Column from '@/components/Column';
|
||||||
@@ -125,20 +126,6 @@ const Home: Component = () => {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/*
|
|
||||||
const { events: searchPosts } = useSubscription(() => ({
|
|
||||||
relayUrls: ['wss://relay.nostr.band/'],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
kinds: [1],
|
|
||||||
search: '#nostrstudy',
|
|
||||||
limit: 25,
|
|
||||||
since: epoch() - 12 * 60 * 60,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
*/
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!persistStatus().loggedIn) {
|
if (!persistStatus().loggedIn) {
|
||||||
navigate('/hello');
|
navigate('/hello');
|
||||||
|
|||||||
Reference in New Issue
Block a user