Repository: rxliuli/mass-block-twitter
Pull request #4: feat: allow modlist autoblocking without logging in
Author: login: catdevnull
Created: 2025-03-20T20:15:47Z | Merged: 2025-03-22T17:11:09Z

this is missing:
- respecting rules
- handling logged out->logged in transition (should we upload the local modlists? or just ignore the local stuff?)
- handling logged in->logged out (actually, this isn't being handled currently, and the existing blocked users ids are kept in storage)


Diff:
diff --git a/packages/plugin/src/entrypoints/content/pages/modlists/components/ModLists.svelte b/packages/plugin/src/entrypoints/content/pages/modlists/components/ModLists.svelte
index 9a6ed13..f08f3af 100644
--- a/packages/plugin/src/entrypoints/content/pages/modlists/components/ModLists.svelte
+++ b/packages/plugin/src/entrypoints/content/pages/modlists/components/ModLists.svelte
@@ -5,6 +5,7 @@
   import type { QueryObserverResult } from '@tanstack/svelte-query'
   import type { ModListSubscribeResponse } from '@mass-block-twitter/server'
   import { navigate } from '$lib/components/logic/router'
+  import { t } from '$lib/i18n';
 
   const {
     query,
@@ -22,6 +23,8 @@
     <QueryLoading />
   {:else if query.error}
     <QueryError description={query.error.message} />
+  {:else if query.data?.length === 0}
+    <p class="text-muted-foreground">{$t('modlists.subscribed.noSubscribed')}</p>
   {:else}
     <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
       {#each query.data ?? [] as list (list.id)}
diff --git a/packages/plugin/src/entrypoints/content/pages/modlists/detail/page.svelte b/packages/plugin/src/entrypoints/content/pages/modlists/detail/page.svelte
index 6b37044..ad622f7 100644
--- a/packages/plugin/src/entrypoints/content/pages/modlists/detail/page.svelte
+++ b/packages/plugin/src/entrypoints/content/pages/modlists/detail/page.svelte
@@ -41,6 +41,7 @@
   import ms from 'ms'
   import { ulid } from 'ulidx'
   import { useModlistUsers } from './utils/useModlistUsers'
+  import * as localModlistSubscriptions from '$lib/localModlistSubscriptions'
   import { t } from '$lib/i18n'
   import { batchBlockUsersMutation } from '$lib/hooks/batchBlockUsers'
 
@@ -66,7 +67,15 @@
           },
         },
       )
-      return (await resp.json()) as ModListGetResponse
+      const modlist = (await resp.json()) as ModListGetResponse
+      if (!authInfo) {
+        const subscription = await localModlistSubscriptions.getSubscription(modListId)
+        if (subscription) {
+          modlist.action = subscription
+          modlist.subscribed = true
+        }
+      }
+      return modlist
     },
   })
   const query = useModlistUsers(route.search?.get('id')!)
@@ -102,16 +111,23 @@
 
   const subscribeMutation = createMutation({
     mutationFn: async (action: ModListSubscribeRequest['action']) => {
+      const modlistId = route.search?.get('id')!
       const authInfo = await getAuthInfo()
       if (!authInfo?.isPro) {
-        const subscribed = (await (
-          await crossFetch(`${SERVER_URL}/api/modlists/subscribed/metadata`, {
-            headers: {
-              Authorization: `Bearer ${authInfo?.token}`,
-            },
-          })
-        ).json()) as ModListSubscribeResponse
-        if (subscribed.length >= 3) {
+        let nSubscribed: number
+        if (authInfo) {
+          const subscribed = (await (
+            await crossFetch(`${SERVER_URL}/api/modlists/subscribed/metadata`, {
+              headers: {
+                Authorization: `Bearer ${authInfo?.token}`,
+              },
+            })
+          ).json()) as ModListSubscribeResponse
+          nSubscribed = subscribed.length
+        } else {
+          nSubscribed = Object.keys(await localModlistSubscriptions.getAllSubscriptions()).length
+        }
+        if (nSubscribed >= 3) {
           toast.info($t('modlists.detail.toast.maxSubscribed'), {
             description: $t('modlists.detail.toast.maxSubscribedDesc'),
             action: {
@@ -123,19 +139,23 @@
           })
         }
       }
-      const resp = await crossFetch(
-        `${SERVER_URL}/api/modlists/subscribe/${route.search?.get('id')}`,
-        {
-          method: 'POST',
-          body: JSON.stringify({ action } satisfies ModListSubscribeRequest),
-          headers: {
-            'Content-Type': 'application/json',
-            Authorization: `Bearer ${authInfo?.token}`,
+      if (authInfo) {
+        const resp = await crossFetch(
+          `${SERVER_URL}/api/modlists/subscribe/${modlistId}`,
+          {
+            method: 'POST',
+            body: JSON.stringify({ action } satisfies ModListSubscribeRequest),
+            headers: {
+              'Content-Type': 'application/json',
+              Authorization: `Bearer ${authInfo?.token}`,
+            },
           },
-        },
-      )
-      if (!resp.ok) {
-        throw resp
+        )
+        if (!resp.ok) {
+          throw resp
+        }
+      } else {
+        await localModlistSubscriptions.setSubscription(modlistId, action)
       }
     },
     onSuccess: async (_data, action) => {
@@ -172,6 +192,10 @@
   const unsubscribeMutation = createMutation({
     mutationFn: async () => {
       const authInfo = await getAuthInfo()
+      if (!authInfo) {
+        await localModlistSubscriptions.removeSubscription(route.search?.get('id')!)
+        return
+      }
       const resp = await crossFetch(
         `${SERVER_URL}/api/modlists/subscribe/${route.search?.get('id')}`,
         {
@@ -283,7 +307,7 @@
     <Button
       variant="outline"
       onclick={() => $unsubscribeMutation.mutate()}
-      disabled={!authInfo.value || $unsubscribeMutation.isPending}
+      disabled={$unsubscribeMutation.isPending}
     >
       {$metadata.data.action === 'block'
         ? $t('modlists.detail.actions.unblock')
@@ -292,7 +316,7 @@
   {:else}
     <DropdownMenu.Root>
       <DropdownMenu.Trigger
-        disabled={!authInfo.value || $subscribeMutation.isPending}
+        disabled={$subscribeMutation.isPending}
       >
         {$t('modlists.detail.actions.subscribe')}
       </DropdownMenu.Trigger>
diff --git a/packages/plugin/src/entrypoints/content/pages/modlists/page.svelte b/packages/plugin/src/entrypoints/content/pages/modlists/page.svelte
index 85d6c3b..9bc257d 100644
--- a/packages/plugin/src/entrypoints/content/pages/modlists/page.svelte
+++ b/packages/plugin/src/entrypoints/content/pages/modlists/page.svelte
@@ -31,10 +31,6 @@
     navigate(`/modlists/created`)
   }
   async function onGotoSubscribed() {
-    if (!authInfo.value) {
-      toast.info($t('modlists.toast.login.subscribed'))
-      return
-    }
     navigate('/modlists/subscribe')
   }
 </script>
diff --git a/packages/plugin/src/entrypoints/content/pages/modlists/subscribe/page.svelte b/packages/plugin/src/entrypoints/content/pages/modlists/subscribe/page.svelte
index 8b5a69b..61debb0 100644
--- a/packages/plugin/src/entrypoints/content/pages/modlists/subscribe/page.svelte
+++ b/packages/plugin/src/entrypoints/content/pages/modlists/subscribe/page.svelte
@@ -6,15 +6,32 @@
   import { SERVER_URL } from '$lib/constants'
   import type { ModListSubscribeResponse } from '@mass-block-twitter/server'
   import { crossFetch } from '$lib/query'
+  import * as localModlistSubscriptions from '$lib/localModlistSubscriptions'
   import { t } from '$lib/i18n'
 
   const query = createQuery({
     queryKey: ['modlists', 'subscribed'],
-    queryFn: async () => {
+    networkMode: 'online',
+    queryFn: async (): Promise<ModListSubscribeResponse> => {
       const authInfo = await getAuthInfo()
+      if (!authInfo) {
+        const localSubs = await localModlistSubscriptions.getAllSubscriptions()
+        const modListIds = Object.keys(localSubs)
+        const modLists = await Promise.all(
+          modListIds.map(async (modListId) => {
+            const resp = await fetch(`${SERVER_URL}/api/modlists/get/${modListId}`)
+            if (resp.ok) {
+              const metadata = await resp.json()
+              return { ...metadata, action: localSubs[modListId] }
+            }
+            return null
+          })
+        )
+        return modLists.filter(Boolean)
+      }
       const resp = await crossFetch(`${SERVER_URL}/api/modlists/subscribed/metadata`, {
         headers: {
-          Authorization: `Bearer ${authInfo?.token}`,
+          Authorization: `Bearer ${authInfo.token}`,
         },
       })
       return (await resp.json()) as ModListSubscribeResponse
diff --git a/packages/plugin/src/i18n/en-US.json b/packages/plugin/src/i18n/en-US.json
index c2ba1f4..21ce94b 100644
--- a/packages/plugin/src/i18n/en-US.json
+++ b/packages/plugin/src/i18n/en-US.json
@@ -48,6 +48,8 @@
   "account.logout.success": "Logged out",
   "account.logout.failed": "Logout failed",
   "account.upgrade.title": "Upgrade",
+  "account.login.toast.subscribeLocal.failed": "Failed to subscribe to previously locally subscribed modlist {modlistId}",
+
 
   "settings.title": "Settings",
   "settings.appearance.title": "Appearance",
@@ -152,7 +154,7 @@
   "modlists.nav.created": "Created",
   "modlists.nav.subscribed": "Subscribed",
   "modlists.toast.login.created": "Please login to view your created modlists",
-  "modlists.toast.login.subscribed": "Please login to view your subscribed modlists",
+  "modlists.subscribed.noSubscribed": "No subscribed modlists",
   "modlists.creator.title": "New Moderation List",
   "modlists.creator.toast.limit.title": "You have reached the maximum number of moderation lists.",
   "modlists.creator.toast.limit.description": "Please upgrade to Pro to create unlimited moderation lists.",
diff --git a/packages/plugin/src/i18n/zh-CN.json b/packages/plugin/src/i18n/zh-CN.json
index e5ba459..e4c5811 100644
--- a/packages/plugin/src/i18n/zh-CN.json
+++ b/packages/plugin/src/i18n/zh-CN.json
@@ -149,7 +149,6 @@
   "modlists.nav.created": "已创建",
   "modlists.nav.subscribed": "已订阅",
   "modlists.toast.login.created": "请登录以查看你创建的审核列表",
-  "modlists.toast.login.subscribed": "请登录以查看你订阅的审核列表",
   "modlists.creator.error.userId": "未找到 Twitter 用户 ID，请重新登录。",
   "modlists.creator.error.userNotFound": "未找到用户，请重新登录。",
 
diff --git a/packages/plugin/src/lib/components/layout/AuthButton.svelte b/packages/plugin/src/lib/components/layout/AuthButton.svelte
index 350ce4b..5e7c012 100644
--- a/packages/plugin/src/lib/components/layout/AuthButton.svelte
+++ b/packages/plugin/src/lib/components/layout/AuthButton.svelte
@@ -3,13 +3,15 @@
   import { openLoginWindow } from '$lib/util/openLoginWindow'
   import { toast } from 'svelte-sonner'
   import * as Sidebar from '$lib/components/ui/sidebar/index.js'
-  import { CircleFadingArrowUpIcon, UserIcon } from 'lucide-svelte'
+  import { CircleFadingArrowUpIcon, LoaderCircle, UserIcon } from 'lucide-svelte'
   import { Badge } from '$lib/components/ui/badge'
   import * as DropdownMenu from '../ui/dropdown-menu'
   import { shadcnConfig } from '../logic/config'
   import { createMutation, useQueryClient } from '@tanstack/svelte-query'
   import { SERVER_URL } from '$lib/constants'
   import { crossFetch } from '$lib/query'
+  import * as localModlistSubscriptions from '$lib/localModlistSubscriptions'
+  import { type ModListSubscribeRequest } from '@mass-block-twitter/server';
   import { t } from '$lib/i18n'
 
   const authInfo = useAuthInfo()
@@ -18,23 +20,59 @@
   const webUrl =
     import.meta.env.VITE_WEB_URL ?? 'https://mass-block-twitter.rxliuli.com'
   const queryClient = useQueryClient()
-  function onLogin() {
-    openLoginWindow(webUrl + '/accounts/login?from=plugin')
-    if (interval) {
-      clearInterval(interval)
-    }
-    interval = setInterval(async () => {
-      const info = await authInfo.getValue()
-      if (!info) {
-        return
+  const loginMutation = createMutation({
+    mutationFn: async () => {
+      openLoginWindow(webUrl + '/accounts/login?from=plugin')
+      if (interval) {
+        clearInterval(interval)
       }
-      authInfo.value = info
-      clearInterval(interval)
+      
+      return new Promise<void>((resolve, reject) => {
+        interval = setInterval(async () => {
+          try {
+            const info = await authInfo.getValue()
+            if (!info) {
+              return
+            }
+            
+            authInfo.value = info
+            clearInterval(interval)
+            
+            const localSubs = await localModlistSubscriptions.getAllSubscriptions()
+            for (const [modlistId, action] of Object.entries(localSubs)) {
+              const resp = await crossFetch(
+                `${SERVER_URL}/api/modlists/subscribe/${modlistId}`,
+                {
+                  method: 'POST',
+                  body: JSON.stringify({ action } satisfies ModListSubscribeRequest),
+                  headers: {
+                    'Content-Type': 'application/json',
+                    Authorization: `Bearer ${info.token}`,
+                  },
+                },
+              )
+              if (!resp.ok) {
+                toast.error($t('account.login.toast.subscribeLocal.failed', {values: {modlistId}}))
+              }
+            }
+            resolve()
+          } catch (error) {
+            clearInterval(interval)
+            reject(error)
+          }
+        }, 1000) as unknown as number
+      });
+    },
+    onSuccess: () => {
       toast.success($t('account.login.success'))
       queryClient.refetchQueries()
-    }, 1000) as unknown as number
-  }
+    },
+    onError: () => {
+      toast.error($t('account.login.failed'))
+    }
+  })
 
+  
   onDestroy(() => {
     if (interval) {
       clearInterval(interval)
@@ -111,11 +149,17 @@
     </DropdownMenu.Content>
   </DropdownMenu.Root>
 {:else}
-  <Sidebar.MenuItem onclick={onLogin}>
+  <Sidebar.MenuItem>
     <Sidebar.MenuButton
       class="bg-blue-500 text-white hover:bg-blue-600 hover:text-white rounded-md transition-colors"
+      onclick={() => $loginMutation.mutate()}
+      disabled={$loginMutation.isPending}
     >
-      <UserIcon />
+      {#if $loginMutation.isPending}
+        <LoaderCircle class="animate-spin" />
+      {:else}
+        <UserIcon />
+      {/if}
       <span>{$t('account.login.title')}</span>
     </Sidebar.MenuButton>
   </Sidebar.MenuItem>
diff --git a/packages/plugin/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/packages/plugin/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
index 1513721..d4e680c 100644
--- a/packages/plugin/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
+++ b/packages/plugin/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
@@ -32,7 +32,7 @@
 	import { cn } from "$lib/utils.js";
 	import { mergeProps, type WithElementRef, type WithoutChildrenOrChild } from "bits-ui";
 	import type { ComponentProps, Snippet } from "svelte";
-	import type { HTMLAttributes } from "svelte/elements";
+	import type { HTMLAttributes, HTMLButtonAttributes } from "svelte/elements";
 	import { useSidebar } from "./context.svelte.js";
 
 	let {
@@ -46,7 +46,7 @@
 		tooltipContent,
 		tooltipContentProps,
 		...restProps
-	}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
+	}: WithElementRef<HTMLButtonAttributes, HTMLButtonElement> & {
 		isActive?: boolean;
 		variant?: SidebarMenuButtonVariant;
 		size?: SidebarMenuButtonSize;
diff --git a/packages/plugin/src/lib/content.ts b/packages/plugin/src/lib/content.ts
index 83d4761..e22489d 100644
--- a/packages/plugin/src/lib/content.ts
+++ b/packages/plugin/src/lib/content.ts
@@ -13,7 +13,8 @@ import { ModListSubscribedUsersKey } from './shared'
 import { crossFetch } from './query'
 import { dbApi } from './db'
 import { chunk } from 'lodash-es'
-import { getSettings } from './settings'
+import * as localModlistSubscriptions from '$lib/localModlistSubscriptions'
+import { type ModListIdsResponse } from '@mass-block-twitter/server'
 
 export async function refreshModListSubscribedUsers(
   force?: boolean,
@@ -22,6 +23,20 @@ export async function refreshModListSubscribedUsers(
     await browser.storage.local.get<{ authInfo: AuthInfo | null }>('authInfo')
   ).authInfo?.token
   if (!token) {
+    const subscriptions = await localModlistSubscriptions.getAllSubscriptions()
+    // TODO: implement fetching rules, currently only twitterUserIds
+    const modlistPromises = Object.entries(subscriptions).map(([modListId, action]) =>
+      fetch(`${SERVER_URL}/api/modlists/ids/${modListId}`)
+        .then((res) => res.json() as Promise<ModListIdsResponse>)
+        .then((data) => ({
+          modListId,
+          action,
+          twitterUserIds: data.twitterUserIds,
+          rules: [],
+        }))
+    );
+    const modlistData = await Promise.all(modlistPromises);
+    await set(ModListSubscribedUsersKey, modlistData);
     return
   }
   const init: RequestInit = {
diff --git a/packages/plugin/src/lib/localModlistSubscriptions.ts b/packages/plugin/src/lib/localModlistSubscriptions.ts
new file mode 100644
index 0000000..a678c85
--- /dev/null
+++ b/packages/plugin/src/lib/localModlistSubscriptions.ts
@@ -0,0 +1,26 @@
+import { ModListSubscribeRequest } from "@mass-block-twitter/server";
+import { get, set, del, entries } from "idb-keyval";
+
+export async function setSubscription(modListId: string, action: ModListSubscribeRequest['action']) {
+  await set(`rxliuli:modlist:${modListId}:action`, action);
+}
+
+export async function removeSubscription(modListId: string) {
+  await del(`rxliuli:modlist:${modListId}:action`);
+}
+
+export async function getSubscription(modListId: string): Promise<ModListSubscribeRequest['action'] | null> {
+  return await get(`rxliuli:modlist:${modListId}:action`) as ModListSubscribeRequest['action'] | null;
+}
+
+export async function getAllSubscriptions(): Promise<Record<string, ModListSubscribeRequest['action']>> {
+  const allEntries = await entries();
+  const subscriptions = allEntries.reduce((acc, [key, value]) => {
+    if (typeof key === 'string' && key.startsWith('rxliuli:modlist:')) {
+      const modListId = key.split(':')[2];
+      acc[modListId] = value as ModListSubscribeRequest['action'];
+    }
+    return acc;
+  }, {} as Record<string, ModListSubscribeRequest['action']>);
+  return subscriptions;
+}
\ No newline at end of file
diff --git a/packages/server/src/lib.d.ts b/packages/server/src/lib.d.ts
index 2082129..019cc62 100644
--- a/packages/server/src/lib.d.ts
+++ b/packages/server/src/lib.d.ts
@@ -49,6 +49,7 @@ export type {
   ModListUpdateRuleResponse,
   ModListAddTwitterUsersResponse,
   ModListAddTwitterUsersRequest,
+  ModListIdsResponse,
 } from './routes/modlists'
 export type { CheckoutCompleteRequest } from './routes/billing'
 export type {
diff --git a/packages/server/src/middlewares/auth.ts b/packages/server/src/middlewares/auth.ts
index 0b68f4b..242b8c3 100644
--- a/packages/server/src/middlewares/auth.ts
+++ b/packages/server/src/middlewares/auth.ts
@@ -32,7 +32,10 @@ export async function generateToken(
 
 export function auth(): MiddlewareHandler<HonoEnv> {
   return async (c, next) => {
-    if (c.req.url === '/api/modlists/search') {
+    if (
+      c.req.url === '/api/modlists/search' ||
+      c.req.url.startsWith('/api/modlists/ids/')
+    ) {
       return next()
     }
     const token = c.req.header('Authorization')?.replace('Bearer ', '')
diff --git a/packages/server/src/routes/modlists.ts b/packages/server/src/routes/modlists.ts
index 7b4a3df..8031d61 100644
--- a/packages/server/src/routes/modlists.ts
+++ b/packages/server/src/routes/modlists.ts
@@ -843,5 +843,36 @@ search.get('/rules', zValidator('query', rulesSchema), async (c) => {
   })
 })
 
+export interface ModListIdsResponse {
+  twitterUserIds: string[]
+}
+search.get(
+  '/ids/:id',
+  zValidator('param', z.object({ id: z.string() })),
+  async (c) => {
+    const id = c.req.param('id')
+    const db = drizzle(c.env.DB)
+
+    const _modList = await db
+      .select()
+      .from(modList)
+      .where(eq(modList.id, id))
+      .limit(1)
+
+    if (_modList.length === 0) {
+      return c.json({ code: 'modListNotFound' }, 404)
+    }
+
+    const users = await db
+      .select({ twitterUserId: modListUser.twitterUserId })
+      .from(modListUser)
+      .where(eq(modListUser.modListId, id))
+
+    return c.json<ModListIdsResponse>({
+      twitterUserIds: users.map((u) => u.twitterUserId),
+    })
+  },
+)
+
 export { modlists, search as modlistSearch }
 export type { ModListConditionItem } from '../db/schema'
diff --git a/packages/server/test/modlists.test.ts b/packages/server/test/modlists.test.ts
index aa22d16..59102ae 100644
--- a/packages/server/test/modlists.test.ts
+++ b/packages/server/test/modlists.test.ts
@@ -23,6 +23,7 @@ import type {
   ModListUpdateRuleResponse,
   ModListAddTwitterUsersRequest,
   ModListAddTwitterUsersResponse,
+  ModListIdsResponse,
 } from '../src/lib'
 import { TwitterUser } from '../src/routes/twitter'
 import { initCloudflareTest } from './utils'
@@ -761,6 +762,27 @@ describe('modlists', () => {
       expect(r2[0].id).toBe('twitter-user-1')
       expect(r2[1].id).toBe('twitter-user-2')
     })
+    it('should be able to get ids', async () => {
+      await addUserToModList(
+        {
+          id: 'twitter-user-1',
+          screen_name: 'test-user-1',
+          name: 'test-user-1',
+          created_at: new Date().toISOString(),
+          is_blue_verified: false,
+          followers_count: 100,
+          friends_count: 100,
+          default_profile: false,
+          default_profile_image: false,
+        },
+        modListId,
+      )
+      const resp1 = await fetch(`/api/modlists/ids/${modListId}`)
+      expect(resp1.ok).true
+      const r1 = (await resp1.json()) as ModListIdsResponse
+      expect(r1.twitterUserIds).length(1)
+      expect(r1.twitterUserIds[0]).toBe('twitter-user-1')
+    })
   })
   describe('rule', () => {
     let modListId: string


Reviews:
user: login: rxliuli
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-22T14:55:35Z
user: login: rxliuli
body: LGTM. I will conduct some local tests and then merge to release a new version.
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-22T15:13:49Z

Review comments:
diff_hunk: @@ -22,6 +23,8 @@
     <QueryLoading />
   {:else if query.error}
     <QueryError description={query.error.message} />
+  {:else if query.data?.length === 0}
+    <p class="text-muted-foreground">{t('modlists.subscribed.noSubscribed')}</p>
path: packages/plugin/src/entrypoints/content/pages/modlists/components/ModLists.svelte
user: login: rxliuli
body: You should use `$t('modlists.subscribed.noSubscribed')` here.
created_at: 2025-03-22T14:45:12Z
author_association: OWNER
diff_hunk: @@ -14,6 +14,8 @@ import { crossFetch } from './query'
 import { dbApi } from './db'
 import { chunk } from 'lodash-es'
 import { getSettings } from './settings'
+import * as localModlistSubscriptions from '$lib/localModlistSubscriptions'
+import { ModListIdsResponse } from 'node_modules/@mass-block-twitter/server/src/routes/modlists'
path: packages/plugin/src/lib/content.ts
user: login: rxliuli
body: The reference path is incorrect, only `import {} from '@mass-block-twitter/server'` is allowed. If an export is missing, please modify _packages/server/src/lib.d.ts_.
created_at: 2025-03-22T14:53:04Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,26 @@
+import { ModListSubscribeRequest } from "@mass-block-twitter/server";
+
+
+
+export async function setSubscription(modListId: string, action: ModListSubscribeRequest['action']) {
+  localStorage.setItem(`rxliuli:modlist:${modListId}:action`, action)
path: packages/plugin/src/lib/localModlistSubscriptions.ts
user: login: rxliuli
body: Please use idb-keyval to store in indexedDB, as localStorage has very limited space.
created_at: 2025-03-22T14:54:45Z
author_association: OWNER

PR comments:
user: login: catdevnull
created_at: 2025-03-20T20:16:37Z
body: another approach to do this could be have it automatically create a virtual user in the backend instead of handling it locally. so we have the benefit of not having the user log in but still be able to do everything
author_association: CONTRIBUTOR
user: login: rxliuli
created_at: 2025-03-20T23:37:20Z
body: > respecting rules

It can be ignored when not logged in, and a confirmation prompt can be added.

> handling logged out->logged in transition (should we upload the local modlists? or just ignore the local stuff?)

Please be sure to automatically upload the locally subscribed review list after logging in, which can be achieved by repeatedly calling the `/api/modlists/subscribe/:modListId` interface.

> handling logged in->logged out (actually, this isn't being handled currently, and the existing blocked users ids are kept in storage)

It can be temporarily ignored.
author_association: OWNER
user: login: rxliuli
created_at: 2025-03-20T23:39:44Z
body: There are also some code conflicts that need to be resolved.
https://github.com/rxliuli/mass-block-twitter/pull/4/conflicts
author_association: OWNER
user: login: rxliuli
created_at: 2025-03-22T00:41:59Z
body: @catdevnull  Um, I just noticed that your Twitter account is gone, are you okay?
<img width="621" alt="image" src="https://github.com/user-attachments/assets/e6461a65-3232-4ad5-96a4-3b50bc58be95" />

author_association: OWNER
user: login: catdevnull
created_at: 2025-03-22T00:43:52Z
body: all good! just deactivated for a bit. will get back to this PR v soon
author_association: CONTRIBUTOR
user: login: catdevnull
created_at: 2025-03-22T14:38:50Z
body: @rxliuli pls review
author_association: CONTRIBUTOR