personal web client for Bluesky
typescript solidjs bluesky atcute

refactor: moderation as singleton service

mary.my.id ae0de252 818299ab

verified
+4 -2
src/api/queries/timeline.ts
··· 14 14 import { globalEvents } from '~/globals/events'; 15 15 16 16 import { useAgent } from '~/lib/states/agent'; 17 - import { useModerationOptions } from '~/lib/states/moderation'; 18 17 import { useSession } from '~/lib/states/session'; 18 + import { inject } from '~/lib/states/singleton'; 19 + import ModerationService from '~/lib/states/singletons/moderation'; 19 20 import { tinyhash } from '~/lib/utils/hash'; 20 21 import { assert } from '~/lib/utils/invariant'; 21 22 ··· 132 133 const { rpc } = useAgent(); 133 134 const { currentAccount } = useSession(); 134 135 const queryClient = useQueryClient(); 135 - const moderationOptions = useModerationOptions(); 136 + 137 + const moderationOptions = inject(ModerationService); 136 138 137 139 const limit = MAX_TIMELINE_POSTS; 138 140
+3 -2
src/components/bookmarks/bookmark-feed-item.tsx
··· 12 12 13 13 import type { HydratedBookmarkItem } from '~/lib/aglais-bookmarks/db'; 14 14 import { isElementAltClicked, isElementClicked } from '~/lib/interaction'; 15 - import { useModerationOptions } from '~/lib/states/moderation'; 15 + import { inject } from '~/lib/states/singleton'; 16 + import ModerationService from '~/lib/states/singletons/moderation'; 16 17 17 18 import Avatar, { getUserAvatarType } from '../avatar'; 18 19 import Embed from '../embeds/embed'; ··· 29 30 } 30 31 31 32 const BookmarkFeedItem = ({ item }: BookmarkFeedItemProps) => { 32 - const moderationOptions = useModerationOptions(); 33 + const moderationOptions = inject(ModerationService); 33 34 34 35 const { post, stale } = item; 35 36
+3 -2
src/components/composer/composer-reply-context.tsx
··· 8 8 import { createPostQuery } from '~/api/queries/post'; 9 9 import { formatQueryError } from '~/api/utils/error'; 10 10 11 - import { useModerationOptions } from '~/lib/states/moderation'; 11 + import { inject } from '~/lib/states/singleton'; 12 + import ModerationService from '~/lib/states/singletons/moderation'; 12 13 13 14 import Avatar, { getUserAvatarType } from '../avatar'; 14 15 import Button from '../button'; ··· 23 24 } 24 25 25 26 const ComposerReplyContext = (props: ComposerReplyContextProps) => { 26 - const moderationOptions = useModerationOptions(); 27 + const moderationOptions = inject(ModerationService); 27 28 28 29 const query = createPostQuery(() => props.replyUri); 29 30
+4 -2
src/components/embeds/feed-embed.tsx
··· 7 7 import { precacheFeed } from '~/api/queries-cache/feed-precache'; 8 8 import { parseAtUri } from '~/api/utils/strings'; 9 9 10 - import { useModerationOptions } from '~/lib/states/moderation'; 10 + import { inject } from '~/lib/states/singleton'; 11 + import ModerationService from '~/lib/states/singletons/moderation'; 11 12 12 13 import Avatar from '../avatar'; 13 14 ··· 20 21 21 22 const FeedEmbed = ({ feed, interactive }: FeedEmbedProps) => { 22 23 const queryClient = useQueryClient(); 23 - const moderationOptions = useModerationOptions(); 24 + 25 + const moderationOptions = inject(ModerationService); 24 26 25 27 const moderation = createMemo(() => moderateGeneric(feed, feed.creator.did, moderationOptions())); 26 28
+4 -2
src/components/embeds/list-embed.tsx
··· 4 4 5 5 import { moderateGeneric } from '~/api/moderation/entities/generic'; 6 6 7 - import { useModerationOptions } from '~/lib/states/moderation'; 7 + import { inject } from '~/lib/states/singleton'; 8 + import ModerationService from '~/lib/states/singletons/moderation'; 8 9 9 10 import Avatar from '~/components/avatar'; 10 11 import { getListPurposeLabel, getListUrl } from '~/components/lists/lib/utils'; ··· 18 19 } 19 20 20 21 const ListEmbed = ({ list, interactive, onClick }: ListEmbedProps) => { 21 - const moderationOptions = useModerationOptions(); 22 + const moderationOptions = inject(ModerationService); 23 + 22 24 const moderation = createMemo(() => moderateGeneric(list, list.creator.did, moderationOptions())); 23 25 24 26 const href = getListUrl(list);
+4 -2
src/components/embeds/quote-embed.tsx
··· 13 13 import { moderateQuote } from '~/api/moderation/entities/quote'; 14 14 import { parseAtUri } from '~/api/utils/strings'; 15 15 16 - import { useModerationOptions } from '~/lib/states/moderation'; 16 + import { inject } from '~/lib/states/singleton'; 17 + import ModerationService from '~/lib/states/singletons/moderation'; 17 18 18 19 import Avatar, { getUserAvatarType } from '../avatar'; 19 20 import TimeAgo from '../time-ago'; ··· 31 32 } 32 33 33 34 const QuoteEmbed = ({ quote, interactive, large }: QuoteEmbedProps) => { 35 + const moderationOptions = inject(ModerationService); 36 + 34 37 const record = quote.value as AppBskyFeedPost.Record; 35 38 const embed = quote.embeds?.[0]; 36 39 const author = quote.author; ··· 42 45 const image = getPostImage(embed); 43 46 const video = getPostVideo(embed); 44 47 45 - const moderationOptions = useModerationOptions(); 46 48 const moderation = createMemo(() => moderateQuote(quote, moderationOptions())); 47 49 48 50 const shouldBlurMedia = () => getModerationUI(moderation(), ContextContentMedia).b.length !== 0;
+4 -2
src/components/feeds/feed-item.tsx
··· 11 11 12 12 import { isElementAltClicked, isElementClicked } from '~/lib/interaction'; 13 13 import { formatLong } from '~/lib/intl/number'; 14 - import { useModerationOptions } from '~/lib/states/moderation'; 14 + import { inject } from '~/lib/states/singleton'; 15 + import ModerationService from '~/lib/states/singletons/moderation'; 15 16 16 17 import Avatar from '~/components/avatar'; 17 18 ··· 22 23 23 24 const FeedItem = ({ item }: FeedItemProps) => { 24 25 const queryClient = useQueryClient(); 25 - const moderationOptions = useModerationOptions(); 26 + 27 + const moderationOptions = inject(ModerationService); 26 28 27 29 const creator = item.creator; 28 30 const href = `/${creator.did}/feeds/${parseAtUri(item.uri).rkey}`;
+4 -2
src/components/lists/list-item.tsx
··· 9 9 import { history } from '~/globals/navigation'; 10 10 11 11 import { isElementAltClicked, isElementClicked } from '~/lib/interaction'; 12 - import { useModerationOptions } from '~/lib/states/moderation'; 12 + import { inject } from '~/lib/states/singleton'; 13 + import ModerationService from '~/lib/states/singletons/moderation'; 13 14 14 15 import Avatar from '~/components/avatar'; 15 16 import BlockOutlinedIcon from '~/components/icons-central/block-outline'; ··· 24 25 25 26 const ListItem = ({ item }: ListItemProps) => { 26 27 const queryClient = useQueryClient(); 27 - const moderationOptions = useModerationOptions(); 28 + 29 + const moderationOptions = inject(ModerationService); 28 30 29 31 const creator = item.creator; 30 32 const href = getListUrl(item);
+4 -2
src/components/notifications/notification-item.tsx
··· 21 21 import { history } from '~/globals/navigation'; 22 22 23 23 import { INTERACTION_TAGS, isElementAltClicked, isElementClicked } from '~/lib/interaction'; 24 - import { useModerationOptions } from '~/lib/states/moderation'; 25 24 import { useSession } from '~/lib/states/session'; 25 + import { inject } from '~/lib/states/singleton'; 26 + import ModerationService from '~/lib/states/singletons/moderation'; 26 27 import { assert } from '~/lib/utils/invariant'; 27 28 import { truncateMiddle } from '~/lib/utils/strings'; 28 29 ··· 150 151 151 152 const renderAvatars = (notifs: AppBskyNotificationListNotifications.Notification[]) => { 152 153 const queryClient = useQueryClient(); 153 - const moderationOptions = useModerationOptions(); 154 + 155 + const moderationOptions = inject(ModerationService); 154 156 155 157 const avatars = notifs.slice(0, MAX_AVATARS).map(({ author }) => { 156 158 const { did, avatar, displayName, handle } = author;
+3 -2
src/components/profiles/profile-item-pressable.tsx
··· 5 5 import { moderateProfile } from '~/api/moderation/entities/profile'; 6 6 7 7 import { isElementClicked } from '~/lib/interaction'; 8 - import { useModerationOptions } from '~/lib/states/moderation'; 8 + import { inject } from '~/lib/states/singleton'; 9 + import ModerationService from '~/lib/states/singletons/moderation'; 9 10 10 11 import Avatar, { getUserAvatarType } from '../avatar'; 11 12 ··· 21 22 } 22 23 23 24 const ProfileItemPressable = (props: ProfileItemPressableProps) => { 24 - const moderationOptions = useModerationOptions(); 25 + const moderationOptions = inject(ModerationService); 25 26 26 27 const profile = props.item; 27 28 const onClick = props.onClick;
+3 -2
src/components/profiles/profile-item.tsx
··· 9 9 import { history } from '~/globals/navigation'; 10 10 11 11 import { isElementAltClicked, isElementClicked } from '~/lib/interaction'; 12 - import { useModerationOptions } from '~/lib/states/moderation'; 12 + import { inject } from '~/lib/states/singleton'; 13 + import ModerationService from '~/lib/states/singletons/moderation'; 13 14 14 15 import Avatar, { getUserAvatarType } from '../avatar'; 15 16 ··· 22 23 23 24 const ProfileItem = (props: ProfileItemProps) => { 24 25 const queryClient = useQueryClient(); 25 - const moderationOptions = useModerationOptions(); 26 + const moderationOptions = inject(ModerationService); 26 27 27 28 const profile = props.item; 28 29
+4 -2
src/components/profiles/profile-view-header.tsx
··· 11 11 import { openModal } from '~/globals/modals'; 12 12 13 13 import { formatCompact } from '~/lib/intl/number'; 14 - import { useModerationOptions } from '~/lib/states/moderation'; 15 14 import { useSession } from '~/lib/states/session'; 15 + import { inject } from '~/lib/states/singleton'; 16 + import ModerationService from '~/lib/states/singletons/moderation'; 16 17 import { truncateMiddle } from '~/lib/utils/strings'; 17 18 18 19 import DefaultLabelerAvatar from '~/assets/default-labeler-avatar.svg?url'; ··· 44 45 }; 45 46 46 47 const ProfileViewHeader = (props: ProfileViewHeader) => { 47 - const moderationOptions = useModerationOptions(); 48 48 const { currentAccount } = useSession(); 49 + 50 + const moderationOptions = inject(ModerationService); 49 51 50 52 const data = () => props.data; 51 53 const viewer = () => data().viewer;
+4 -2
src/components/threads/highlighted-post.tsx
··· 14 14 15 15 import { formatCompact } from '~/lib/intl/number'; 16 16 import type { ContentTranslationPreferences } from '~/lib/preferences/account'; 17 - import { useModerationOptions } from '~/lib/states/moderation'; 18 17 import { useSession } from '~/lib/states/session'; 18 + import { inject } from '~/lib/states/singleton'; 19 + import ModerationService from '~/lib/states/singletons/moderation'; 19 20 20 21 import Avatar, { getUserAvatarType } from '../avatar'; 21 22 import ComposerDialogLazy from '../composer/composer-dialog-lazy'; ··· 52 53 const HighlightedPost = (props: HighlightedPostProps) => { 53 54 const post = () => props.post; 54 55 55 - const moderationOptions = useModerationOptions(); 56 56 const { currentAccount } = useSession(); 57 + 58 + const moderationOptions = inject(ModerationService); 57 59 58 60 const author = () => post().author; 59 61 const record = () => post().record as AppBskyFeedPost.Record;
+4 -2
src/components/threads/post-thread-item.tsx
··· 14 14 import { history } from '~/globals/navigation'; 15 15 16 16 import { isElementAltClicked, isElementClicked } from '~/lib/interaction'; 17 - import { useModerationOptions } from '~/lib/states/moderation'; 18 17 import { useSession } from '~/lib/states/session'; 18 + import { inject } from '~/lib/states/singleton'; 19 + import ModerationService from '~/lib/states/singletons/moderation'; 19 20 20 21 import Avatar, { getUserAvatarType } from '../avatar'; 21 22 import Embed from '../embeds/embed'; ··· 41 42 const item = () => props.item; 42 43 43 44 const queryClient = useQueryClient(); 44 - const moderationOptions = useModerationOptions(); 45 45 const { currentAccount } = useSession(); 46 + 47 + const moderationOptions = inject(ModerationService); 46 48 47 49 const post = () => item().post; 48 50
+4 -2
src/components/timeline/post-feed-item.tsx
··· 14 14 import { history } from '~/globals/navigation'; 15 15 16 16 import { isElementAltClicked, isElementClicked } from '~/lib/interaction'; 17 - import { useModerationOptions } from '~/lib/states/moderation'; 18 17 import { useSession } from '~/lib/states/session'; 18 + import { inject } from '~/lib/states/singleton'; 19 + import ModerationService from '~/lib/states/singletons/moderation'; 19 20 20 21 import Avatar, { getUserAvatarType } from '../avatar'; 21 22 import Embed from '../embeds/embed'; ··· 40 41 41 42 const PostFeedItem = ({ item, highlighted, timelineDid }: PostFeedItemProps) => { 42 43 const queryClient = useQueryClient(); 43 - const moderationOptions = useModerationOptions(); 44 44 const { currentAccount } = useSession(); 45 + 46 + const moderationOptions = inject(ModerationService); 45 47 46 48 const { post, reason, next, prev } = item; 47 49
+19 -29
src/lib/states/moderation.tsx src/lib/states/singletons/moderation.ts
··· 1 - import { type ParentProps, createContext, createMemo, useContext } from 'solid-js'; 1 + import { createMemo } from 'solid-js'; 2 2 import { unwrap } from 'solid-js/store'; 3 3 4 4 import type { AppBskyLabelerDefs, At } from '@atcute/client/lexicons'; ··· 10 10 import { interpretLabelerDefinition } from '~/api/moderation/labeler'; 11 11 12 12 import { createBatchedFetch } from '~/lib/utils/batch-fetch'; 13 - import { assert } from '~/lib/utils/invariant'; 14 13 15 - import { useAgent } from './agent'; 16 - import { useSession } from './session'; 14 + import { useAgent } from '../agent'; 15 + import { useSession } from '../session'; 17 16 18 17 type Labeler = AppBskyLabelerDefs.LabelerViewDetailed; 19 18 20 - const DEFAULT_MODERATION_PREFERENCES: ModerationPreferences = { 21 - hideReposts: [], 22 - keywords: [], 23 - labelers: { 24 - [BLUESKY_MODERATION_DID]: { 25 - redact: true, 26 - privileged: true, 27 - labels: {}, 28 - }, 29 - }, 30 - labels: {}, 31 - }; 32 - 33 - const Context = createContext<() => ModerationOptions>(); 34 - 35 - export const ModerationProvider = (props: ParentProps) => { 19 + const ModerationService = () => { 36 20 const { rpc, persister } = useAgent(); 37 21 const { currentAccount } = useSession(); 38 22 39 - const modPreferences = createMemo(() => { 23 + const modPreferences = createMemo((): ModerationPreferences => { 40 24 if (!currentAccount) { 41 - return DEFAULT_MODERATION_PREFERENCES; 25 + return { 26 + hideReposts: [], 27 + keywords: [], 28 + labelers: { 29 + [BLUESKY_MODERATION_DID]: { 30 + redact: true, 31 + privileged: true, 32 + labels: {}, 33 + }, 34 + }, 35 + labels: {}, 36 + }; 42 37 } 43 38 44 39 return currentAccount.preferences.moderation; ··· 94 89 }; 95 90 }); 96 91 97 - return <Context.Provider value={modOptions}>{props.children}</Context.Provider>; 92 + return modOptions; 98 93 }; 99 94 100 - export const useModerationOptions = (): (() => ModerationOptions) => { 101 - const options = useContext(Context); 102 - assert(options !== undefined, `Expected useModerationOptions to be used under <ModerationProvider>`); 103 - 104 - return options; 105 - }; 95 + export default ModerationService;
+4 -7
src/main.tsx
··· 10 10 11 11 import { configureRouter } from '~/lib/navigation/router'; 12 12 import { AgentProvider } from '~/lib/states/agent'; 13 - import { ModerationProvider } from '~/lib/states/moderation'; 14 13 import { SessionProvider, useSession } from '~/lib/states/session'; 15 14 import { SingletonProvider } from '~/lib/states/singleton'; 16 15 import { ThemeProvider } from '~/lib/states/theme'; ··· 76 75 return ( 77 76 <AgentProvider> 78 77 {/* Anything under <AgentProvider> gets remounted on account changes */} 79 - <ModerationProvider> 80 - <SingletonProvider> 81 - <Shell /> 82 - <ModalRenderer /> 83 - </SingletonProvider> 84 - </ModerationProvider> 78 + <SingletonProvider> 79 + <Shell /> 80 + <ModalRenderer /> 81 + </SingletonProvider> 85 82 </AgentProvider> 86 83 ); 87 84 }) as unknown as JSX.Element;
+4 -2
src/views/moderation.tsx
··· 3 3 4 4 import { Key } from '~/lib/keyed'; 5 5 import { useTitle } from '~/lib/navigation/router'; 6 - import { useModerationOptions } from '~/lib/states/moderation'; 7 6 import { useSession } from '~/lib/states/session'; 7 + import { inject } from '~/lib/states/singleton'; 8 + import ModerationService from '~/lib/states/singletons/moderation'; 8 9 9 10 import Avatar from '~/components/avatar'; 10 11 import * as Boxed from '~/components/boxed'; ··· 20 21 const ModerationPage = () => { 21 22 const { currentAccount } = useSession(); 22 23 23 - const hydratedOptions = useModerationOptions(); 24 + const hydratedOptions = inject(ModerationService); 25 + 24 26 const moderation = currentAccount!.preferences.moderation; 25 27 26 28 useTitle(() => `Moderation — ${import.meta.env.VITE_APP_NAME}`);
+4 -2
src/views/post-thread.tsx
··· 18 18 import { createEventListener } from '~/lib/hooks/event-listener'; 19 19 import { Key } from '~/lib/keyed'; 20 20 import { useParams, useTitle } from '~/lib/navigation/router'; 21 - import { useModerationOptions } from '~/lib/states/moderation'; 22 21 import { useSession } from '~/lib/states/session'; 22 + import { inject } from '~/lib/states/singleton'; 23 + import ModerationService from '~/lib/states/singletons/moderation'; 23 24 import { truncateMiddle } from '~/lib/utils/strings'; 24 25 25 26 import Button from '~/components/button'; ··· 221 222 onMainPostDelete?: () => void; 222 223 }) => { 223 224 const { currentAccount } = useSession(); 224 - const moderationOptions = useModerationOptions(); 225 + 226 + const moderationOptions = inject(ModerationService); 225 227 226 228 const [showTl, setShowTl] = createSignal(false); 227 229
+4 -2
src/views/profile-feed-info.tsx
··· 11 11 import { makeAtUri } from '~/api/utils/strings'; 12 12 13 13 import { useParams, useTitle } from '~/lib/navigation/router'; 14 - import { useModerationOptions } from '~/lib/states/moderation'; 14 + import { inject } from '~/lib/states/singleton'; 15 + import ModerationService from '~/lib/states/singletons/moderation'; 15 16 16 17 import Avatar, { getUserAvatarType } from '~/components/avatar'; 17 18 import * as Boxed from '~/components/boxed'; ··· 55 56 export default FeedInfoPage; 56 57 57 58 const InfoView = (props: { feed: AppBskyFeedDefs.GeneratorView }) => { 58 - const moderationOptions = useModerationOptions(); 59 59 const queryClient = useQueryClient(); 60 + 61 + const moderationOptions = inject(ModerationService); 60 62 61 63 const feed = () => props.feed; 62 64 const creator = () => feed().creator;
+4 -2
src/views/profile-moderation-list.tsx
··· 13 13 import { makeAtUri } from '~/api/utils/strings'; 14 14 15 15 import { useParams, useTitle } from '~/lib/navigation/router'; 16 - import { useModerationOptions } from '~/lib/states/moderation'; 16 + import { inject } from '~/lib/states/singleton'; 17 + import ModerationService from '~/lib/states/singletons/moderation'; 17 18 18 19 import Avatar, { getUserAvatarType } from '~/components/avatar'; 19 20 import * as Boxed from '~/components/boxed'; ··· 99 100 export default ProfileModerationListPage; 100 101 101 102 const InfoView = (props: { list: AppBskyGraphDefs.ListView }) => { 102 - const moderationOptions = useModerationOptions(); 103 103 const queryClient = useQueryClient(); 104 + 105 + const moderationOptions = inject(ModerationService); 104 106 105 107 const list = () => props.list; 106 108 const creator = () => list().creator;