mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 22:44:26 +01:00
feat: follow and mute
This commit is contained in:
@@ -3,6 +3,7 @@ import { createSignal, For, type JSX } from 'solid-js';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import UserNameDisplay from './UserDisplayName';
|
||||
|
||||
type ConfigProps = {
|
||||
onClose: () => void;
|
||||
@@ -21,7 +22,7 @@ const RelayConfig = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">リレー</h3>
|
||||
<ul>
|
||||
<For each={config().relayUrls}>
|
||||
@@ -83,7 +84,7 @@ const DateFormatConfig = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">時刻の表記</h3>
|
||||
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
|
||||
<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 { config, setConfig } = useConfig();
|
||||
|
||||
@@ -150,7 +213,7 @@ const OtherConfig = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">その他</h3>
|
||||
<div class="flex flex-col justify-evenly gap-2">
|
||||
<div class="flex w-full">
|
||||
@@ -194,6 +257,7 @@ const ConfigUI = (props: ConfigProps) => {
|
||||
<RelayConfig />
|
||||
<DateFormatConfig />
|
||||
<OtherConfig />
|
||||
<MuteConfig />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,9 @@ export type ContextMenuProps = {
|
||||
};
|
||||
|
||||
export type MenuDisplayProps = {
|
||||
menuRef: (elem: HTMLUListElement) => void;
|
||||
menu: MenuItem[];
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
@@ -37,7 +39,11 @@ const MenuItemDisplay: Component<MenuItemDisplayProps> = (props) => {
|
||||
|
||||
const MenuDisplay: Component<MenuDisplayProps> = (props) => {
|
||||
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}>
|
||||
{(item) => <MenuItemDisplay item={item} onClose={props.onClose} />}
|
||||
</For>
|
||||
@@ -46,7 +52,7 @@ const MenuDisplay: Component<MenuDisplayProps> = (props) => {
|
||||
};
|
||||
|
||||
const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||
let menuRef: HTMLDivElement | undefined;
|
||||
let menuRef: HTMLUListElement | undefined;
|
||||
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
@@ -66,8 +72,9 @@ const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
if (menuRef == null) return;
|
||||
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
menuRef.style.left = `${buttonRect.left}px`;
|
||||
const buttonRect = ev.currentTarget.getBoundingClientRect();
|
||||
const menuRect = menuRef.getBoundingClientRect();
|
||||
menuRef.style.left = `${buttonRect.left - buttonRect.width}px`;
|
||||
menuRef.style.top = `${buttonRect.top + buttonRect.height}px`;
|
||||
|
||||
setIsOpen(true);
|
||||
@@ -86,13 +93,14 @@ const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>{props.children}</button>
|
||||
<div
|
||||
ref={menuRef}
|
||||
class="absolute z-20 min-w-[48px] rounded border bg-white shadow-md"
|
||||
classList={{ hidden: !isOpen(), block: isOpen() }}
|
||||
>
|
||||
<MenuDisplay menu={props.menu} onClose={() => setIsOpen(false)} />
|
||||
</div>
|
||||
<MenuDisplay
|
||||
menuRef={(e) => {
|
||||
menuRef = e;
|
||||
}}
|
||||
menu={props.menu}
|
||||
isOpen={isOpen()}
|
||||
onClose={() => setIsOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
import CheckCircle from 'heroicons/24/solid/check-circle.svg';
|
||||
import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.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 Timeline from '@/components/Timeline';
|
||||
@@ -17,11 +19,14 @@ import useVerification from '@/nostr/useVerification';
|
||||
import useFollowings from '@/nostr/useFollowings';
|
||||
import useFollowers from '@/nostr/useFollowers';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useCommands from '@/nostr/useCommands';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import epoch from '@/utils/epoch';
|
||||
import timeout from '@/utils/timeout';
|
||||
import ContextMenu, { MenuItem } from './ContextMenu';
|
||||
|
||||
export type ProfileDisplayProps = {
|
||||
pubkey: string;
|
||||
@@ -37,8 +42,11 @@ const FollowersCount: Component<{ pubkey: string }> = (props) => {
|
||||
};
|
||||
|
||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
const { config } = useConfig();
|
||||
const pubkey = usePubkey();
|
||||
const { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted } = useConfig();
|
||||
const commands = useCommands();
|
||||
const myPubkey = usePubkey();
|
||||
|
||||
const npub = createMemo(() => npubEncodeFallback(props.pubkey));
|
||||
|
||||
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
||||
const [showFollowers, setShowFollowers] = createSignal(false);
|
||||
@@ -58,25 +66,103 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
return { user, domain, ident };
|
||||
};
|
||||
const isVerified = () => verification()?.pubkey === props.pubkey;
|
||||
const isMuted = () => isPubkeyMuted(props.pubkey);
|
||||
|
||||
const { followingPubkeys: myFollowingPubkeys } = useFollowings(() =>
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
|
||||
const {
|
||||
followingPubkeys: myFollowingPubkeys,
|
||||
invalidateFollowings: invalidateMyFollowings,
|
||||
query: myFollowingQuery,
|
||||
} = useFollowings(() =>
|
||||
ensureNonNull([myPubkey()] as const)(([pubkeyNonNull]) => ({
|
||||
pubkey: pubkeyNonNull,
|
||||
})),
|
||||
);
|
||||
const following = () => myFollowingPubkeys().includes(props.pubkey);
|
||||
|
||||
const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings(
|
||||
() => ({
|
||||
pubkey: props.pubkey,
|
||||
}),
|
||||
() => ({ pubkey: props.pubkey }),
|
||||
);
|
||||
|
||||
const followed = () => {
|
||||
const p = pubkey();
|
||||
const p = myPubkey();
|
||||
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(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
@@ -113,100 +199,118 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
)}
|
||||
</Show>
|
||||
<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">
|
||||
<Show when={profile()?.picture} keyed>
|
||||
{(pictureUrl) => (
|
||||
<img
|
||||
src={pictureUrl}
|
||||
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>
|
||||
<div class="flex-1 shrink-0">
|
||||
<div class="h-28 w-28 rounded-lg shadow-md">
|
||||
<Show when={profile()?.picture} keyed>
|
||||
{(pictureUrl) => (
|
||||
<img
|
||||
src={pictureUrl}
|
||||
alt="user icon"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
</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 class="flex shrink-0 flex-col items-center justify-center gap-1">
|
||||
{/*
|
||||
<Switch
|
||||
fallback={
|
||||
<button
|
||||
class="w-24 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
>
|
||||
フォロー
|
||||
</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>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-col items-center gap-1">
|
||||
<div class="flex flex-row justify-start gap-1">
|
||||
<Switch>
|
||||
<Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}>
|
||||
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
||||
読み込み中
|
||||
</span>
|
||||
</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()}>
|
||||
<button
|
||||
class="w-32 rounded-full border border-primary bg-primary px-4 py-2
|
||||
text-center font-bold text-white hover:bg-rose-500"
|
||||
class="rounded-full border border-primary bg-primary px-4 py-2
|
||||
text-center font-bold text-white hover:bg-rose-500 sm:w-32"
|
||||
onMouseEnter={() => setHoverFollowButton(true)}
|
||||
onMouseLeave={() => setHoverFollowButton(false)}
|
||||
onClick={() => unfollow()}
|
||||
disabled={updateContactsMutation.isLoading}
|
||||
>
|
||||
<Show when={!hoverFollowButton()} fallback="フォロー解除">
|
||||
フォロー中
|
||||
</Show>
|
||||
</button>
|
||||
</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>
|
||||
*/}
|
||||
<Show when={followed()}>
|
||||
<div class="shrink-0 text-xs">フォローされています</div>
|
||||
<ContextMenu menu={menu}>
|
||||
<button
|
||||
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 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>
|
||||
<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}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { type Component } from 'solid-js';
|
||||
import { Show, type Component } from 'solid-js';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay';
|
||||
|
||||
export type TextNoteProps = TextNoteDisplayProps;
|
||||
|
||||
const TextNote: Component<TextNoteProps> = (props) => {
|
||||
const { shouldMuteEvent } = useConfig();
|
||||
|
||||
return (
|
||||
<ColumnItem>
|
||||
<TextNoteDisplay {...props} />
|
||||
</ColumnItem>
|
||||
<Show when={!shouldMuteEvent(props.event)}>
|
||||
<ColumnItem>
|
||||
<TextNoteDisplay {...props} />
|
||||
</ColumnItem>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
{
|
||||
content: () => 'JSONとしてコピー',
|
||||
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),
|
||||
onSuccess: () => {
|
||||
console.log('succeeded to publish reaction');
|
||||
invalidateReactions().catch((err) => console.error('failed to refetch reactions', err));
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error('failed to publish reaction: ', err);
|
||||
},
|
||||
onSettled: () => {
|
||||
invalidateReactions().catch((err) => console.error('failed to refetch reactions', err));
|
||||
},
|
||||
});
|
||||
|
||||
const publishDeprecatedRepostMutation = createMutation({
|
||||
@@ -106,11 +110,13 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
mutationFn: commands.publishDeprecatedRepost.bind(commands),
|
||||
onSuccess: () => {
|
||||
console.log('succeeded to publish reposts');
|
||||
invalidateDeprecatedReposts().catch((err) => console.error('failed to refetch reposts', err));
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error('failed to publish repost: ', err);
|
||||
},
|
||||
onSettled: () => {
|
||||
invalidateDeprecatedReposts().catch((err) => console.error('failed to refetch reposts', err));
|
||||
},
|
||||
});
|
||||
|
||||
const isReactedByMe = createMemo(() => {
|
||||
|
||||
@@ -14,14 +14,21 @@ type TextNoteDisplayByIdProps = Omit<TextNoteDisplayProps, 'event'> & {
|
||||
};
|
||||
|
||||
const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
|
||||
const { shouldMuteEvent } = useConfig();
|
||||
const { event, query: eventQuery } = useEvent(() =>
|
||||
ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({
|
||||
eventId: eventIdNonNull,
|
||||
})),
|
||||
);
|
||||
|
||||
const hidden = (): boolean => {
|
||||
const ev = event();
|
||||
return ev != null && shouldMuteEvent(ev);
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch fallback="投稿が見つかりません">
|
||||
<Match when={hidden()}>{null}</Match>
|
||||
<Match when={event()} keyed>
|
||||
{(ev) => <TextNoteDisplay event={ev} {...props} />}
|
||||
</Match>
|
||||
|
||||
Reference in New Issue
Block a user