diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 97a45337b..25a094e11 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -23,6 +23,7 @@
"@follow/models": "workspace:*",
"@follow/shared": "workspace:*",
"@follow/utils": "workspace:*",
+ "@formatjs/intl-numberformat": "8.15.2",
"@gorhom/portal": "1.0.14",
"@hookform/resolvers": "3.9.1",
"@infinite-list/data-model": "2.2.10",
@@ -36,6 +37,7 @@
"better-auth": "1.1.9-beta.1",
"cookie-es": "^1.2.2",
"dayjs": "1.11.13",
+ "dnum": "^2.14.0",
"es-toolkit": "1.29.0",
"expo": "52.0.18",
"expo-apple-authentication": "~7.1.2",
diff --git a/apps/mobile/src/components/common/Balance.tsx b/apps/mobile/src/components/common/Balance.tsx
new file mode 100644
index 000000000..4d08e09b2
--- /dev/null
+++ b/apps/mobile/src/components/common/Balance.tsx
@@ -0,0 +1,69 @@
+import { cn, toScientificNotation } from "@follow/utils"
+import { Text } from "react-native"
+
+export const Balance = ({
+ children,
+
+ className,
+ precision = 2,
+}: {
+ /** The token balance in wei. */
+ children: bigint
+
+ className?: string
+ precision?: number
+}) => {
+ const n = BigInt(children || 0n)
+ const formatted = format(n, { digits: precision, trailingZeros: true })
+
+ return {formatted}
+}
+
+function format(
+ value: bigint,
+ options:
+ | number
+ | {
+ digits?: number
+ compact?: boolean
+ trailingZeros?: boolean
+ locale?: string
+ decimalsRounding?: "ROUND_HALF" | "ROUND_UP" | "ROUND_DOWN"
+ signDisplay?: "auto" | "always" | "exceptZero" | "negative" | "never"
+ } = {},
+): string {
+ // Normalize options
+ const opts = typeof options === "number" ? { digits: options } : options
+ const {
+ digits,
+ compact = false,
+ trailingZeros = false,
+ locale = "en-US",
+
+ signDisplay = "auto",
+ } = opts
+
+ // Convert bigint to number for formatting
+ const num = Number(value) / 1e18 // Assuming 18 decimals (wei to ether conversion)
+
+ if (compact) {
+ return new Intl.NumberFormat(locale, {
+ notation: "compact",
+ maximumFractionDigits: digits,
+ minimumFractionDigits: trailingZeros ? digits : 0,
+ signDisplay,
+ }).format(num)
+ }
+
+ // For very large or small numbers, use scientific notation
+ if (Math.abs(num) > 1e9 || (Math.abs(num) < 1e-9 && num !== 0)) {
+ return toScientificNotation(num.toString(), 10)
+ }
+
+ return new Intl.NumberFormat(locale, {
+ maximumFractionDigits: digits,
+ minimumFractionDigits: trailingZeros ? digits : 0,
+ signDisplay,
+ useGrouping: true,
+ }).format(num)
+}
diff --git a/apps/mobile/src/components/ui/grouped/GroupedList.tsx b/apps/mobile/src/components/ui/grouped/GroupedList.tsx
index 68b85e6d4..02b5bcf27 100644
--- a/apps/mobile/src/components/ui/grouped/GroupedList.tsx
+++ b/apps/mobile/src/components/ui/grouped/GroupedList.tsx
@@ -5,6 +5,7 @@ import { Fragment } from "react"
import type { ViewProps } from "react-native"
import { Pressable, StyleSheet, Text, View } from "react-native"
+import { MingcuteRightLine } from "@/src/icons/mingcute_right_line"
import { RightCuteReIcon } from "@/src/icons/right_cute_re"
import { useColor } from "@/src/theme/colors"
@@ -91,7 +92,7 @@ export const GroupedInsetListNavigationLink: FC<{
{label}
-
+
@@ -160,3 +161,29 @@ export const GroupedInsetListActionCell: FC<{
)
}
+
+export const GroupedInformationCell: FC<{
+ title: string
+ description?: string
+ icon?: React.ReactNode
+ iconBackgroundColor?: string
+}> = ({ title, description, icon, iconBackgroundColor }) => {
+ return (
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {title}
+ {!!description && (
+
+ {description}
+
+ )}
+
+ )
+}
diff --git a/apps/mobile/src/components/ui/loading/index.tsx b/apps/mobile/src/components/ui/loading/index.tsx
index 65f7f7e78..67dea6b23 100644
--- a/apps/mobile/src/components/ui/loading/index.tsx
+++ b/apps/mobile/src/components/ui/loading/index.tsx
@@ -20,7 +20,7 @@ export const LoadingIndicator: FC<
color?: string
className?: string
} & PropsWithChildren
-> = ({ size = 60, color, children, className }) => {
+> = ({ size = 36, color, children, className }) => {
const rotateValue = useSharedValue(0)
const rotation = useDerivedValue(() => {
diff --git a/apps/mobile/src/icons/user_add_2_cute_fi.tsx b/apps/mobile/src/icons/user_add_2_cute_fi.tsx
new file mode 100644
index 000000000..9e9eca46a
--- /dev/null
+++ b/apps/mobile/src/icons/user_add_2_cute_fi.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface UserAdd2CuteFiIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const UserAdd2CuteFiIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: UserAdd2CuteFiIconProps) => {
+ return (
+
+ )
+}
diff --git a/apps/mobile/src/icons/wallet_2_cute_fi.tsx b/apps/mobile/src/icons/wallet_2_cute_fi.tsx
new file mode 100644
index 000000000..07c26857c
--- /dev/null
+++ b/apps/mobile/src/icons/wallet_2_cute_fi.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface Wallet2CuteFiIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const Wallet2CuteFiIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: Wallet2CuteFiIconProps) => {
+ return (
+
+ )
+}
diff --git a/apps/mobile/src/modules/discover/search-tabs/hooks.tsx b/apps/mobile/src/modules/discover/search-tabs/hooks.tsx
index 0c3e72ab1..39b0e1e7f 100644
--- a/apps/mobile/src/modules/discover/search-tabs/hooks.tsx
+++ b/apps/mobile/src/modules/discover/search-tabs/hooks.tsx
@@ -15,7 +15,7 @@ export const useDataSkeleton = (isLoading: boolean, data: any) => {
return (
-
+
)
}
diff --git a/apps/mobile/src/modules/feed-drawer/feed-panel.tsx b/apps/mobile/src/modules/feed-drawer/feed-panel.tsx
index d00948c70..5dc34415b 100644
--- a/apps/mobile/src/modules/feed-drawer/feed-panel.tsx
+++ b/apps/mobile/src/modules/feed-drawer/feed-panel.tsx
@@ -210,7 +210,7 @@ const SubscriptionItem = memo(({ id, className }: { id: string; className?: stri
if (isLoading) {
return (
-
+
)
}
diff --git a/apps/mobile/src/modules/feed/FollowFeed.tsx b/apps/mobile/src/modules/feed/FollowFeed.tsx
index 8383c1bce..10a496044 100644
--- a/apps/mobile/src/modules/feed/FollowFeed.tsx
+++ b/apps/mobile/src/modules/feed/FollowFeed.tsx
@@ -40,7 +40,7 @@ export function FollowFeed(props: { id: string }) {
if (isLoading) {
return (
-
+
)
}
diff --git a/apps/mobile/src/modules/list/FollowList.tsx b/apps/mobile/src/modules/list/FollowList.tsx
index 139b45c40..ded5a9e28 100644
--- a/apps/mobile/src/modules/list/FollowList.tsx
+++ b/apps/mobile/src/modules/list/FollowList.tsx
@@ -38,7 +38,7 @@ export const FollowList = (props: { id: string }) => {
if (isLoading) {
return (
-
+
)
}
diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx
index e43722972..8766299e3 100644
--- a/apps/mobile/src/modules/settings/SettingsList.tsx
+++ b/apps/mobile/src/modules/settings/SettingsList.tsx
@@ -60,6 +60,7 @@ const UserGroupNavigationLinks: GroupNavigationLink[] = [
navigation.navigate("Achievement")
},
iconBackgroundColor: "#6366F1",
+ todo: true,
},
]
@@ -107,6 +108,7 @@ const DataGroupNavigationLinks: GroupNavigationLink[] = [
navigation.navigate("Actions")
},
iconBackgroundColor: "#059669",
+ todo: true,
},
{
@@ -116,6 +118,7 @@ const DataGroupNavigationLinks: GroupNavigationLink[] = [
navigation.navigate("Feeds")
},
iconBackgroundColor: "#10B981",
+ todo: true,
},
{
label: "Lists",
@@ -124,6 +127,7 @@ const DataGroupNavigationLinks: GroupNavigationLink[] = [
navigation.navigate("Lists")
},
iconBackgroundColor: "#34D399",
+ // todo: true,
},
]
diff --git a/apps/mobile/src/modules/settings/routes/Lists.tsx b/apps/mobile/src/modules/settings/routes/Lists.tsx
index 537f2ec7b..ec098bb3b 100644
--- a/apps/mobile/src/modules/settings/routes/Lists.tsx
+++ b/apps/mobile/src/modules/settings/routes/Lists.tsx
@@ -1,9 +1,168 @@
-import { Text, View } from "react-native"
+import { useQuery } from "@tanstack/react-query"
+import { createElement, useCallback } from "react"
+import type { ListRenderItem } from "react-native"
+import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native"
+import Animated, { LinearTransition } from "react-native-reanimated"
+import { useColor } from "react-native-uikit-colors"
+
+import { Balance } from "@/src/components/common/Balance"
+import {
+ NavigationBlurEffectHeader,
+ SafeNavigationScrollView,
+} from "@/src/components/common/SafeNavigationScrollView"
+import {
+ GroupedInformationCell,
+ GroupedInsetListCard,
+} from "@/src/components/ui/grouped/GroupedList"
+import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon"
+import { LoadingIndicator } from "@/src/components/ui/loading"
+import { views } from "@/src/constants/views"
+import { AddCuteReIcon } from "@/src/icons/add_cute_re"
+import { PowerIcon } from "@/src/icons/power"
+import { RadaCuteFiIcon } from "@/src/icons/rada_cute_fi"
+import { UserAdd2CuteFiIcon } from "@/src/icons/user_add_2_cute_fi"
+import { Wallet2CuteFiIcon } from "@/src/icons/wallet_2_cute_fi"
+import type { HonoApiClient } from "@/src/morph/types"
+import { listSyncServices } from "@/src/store/list/store"
+import { accentColor } from "@/src/theme/colors"
export const ListsScreen = () => {
+ const { isLoading, data } = useQuery({
+ queryKey: ["owned", "lists"],
+ queryFn: () => listSyncServices.fetchOwnedLists(),
+ })
+
return (
-
- Lists Settings
+
+ (
+
+ ),
+ [],
+ )}
+ />
+
+
+
+ }
+ iconBackgroundColor="#34D399"
+ />
+
+
+
+
+ {data && (
+
+
+
+ )}
+
+
+ {isLoading && (
+
+
+
+ )}
+
+ )
+}
+
+const AddListButton = () => {
+ const labelColor = useColor("label")
+ return (
+
+
+
+ )
+}
+
+const ItemSeparatorComponent = () => {
+ return (
+
+ )
+}
+
+const keyExtractor = (item: HonoApiClient.List_List_Get) => item.id
+
+const ListItemCell: ListRenderItem = ({ item: list }) => {
+ const { title, description } = list
+ return (
+
+
+ {list.image ? (
+
+ ) : (
+
+ )}
+
+
+
+ {title}
+
+
+ {description}
+
+
+ {!!views[list.view]?.icon &&
+ createElement(views[list.view]!.icon, {
+ color: views[list.view]!.activeColor,
+ height: 16,
+ width: 16,
+ })}
+ {views[list.view]?.name}
+
+
+
+
+
+
+
+ {list.fee}
+
+
+
+
+ {list.subscriptionCount || 0}
+
+
+ {!!list.purchaseAmount && (
+
+
+
+ {BigInt(list.purchaseAmount)}
+
+
+ )}
+
)
}
+
+const styles = StyleSheet.create({
+ title: {
+ fontSize: 20,
+ fontWeight: "semibold",
+ },
+})
diff --git a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx
index 1c4ec4bce..fe993ac7f 100644
--- a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx
+++ b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx
@@ -22,6 +22,7 @@ import { UnGroupedList } from "./UnGroupedList"
// })
// }
export const CategoryGrouped = memo(
+ // eslint-disable-next-line @eslint-react/no-unstable-context-value
({ category, subscriptionIds }: { category: string; subscriptionIds: string[] }) => {
const unreadCounts = useUnreadCounts(subscriptionIds)
const [expanded, setExpanded] = useState(false)
diff --git a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx
index dd6a612f3..957795e05 100644
--- a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx
+++ b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx
@@ -54,7 +54,7 @@ export const SubscriptionItem = memo(({ id, className }: { id: string; className
if (isLoading) {
return (
-
+
)
}
diff --git a/apps/mobile/src/morph/hono.ts b/apps/mobile/src/morph/hono.ts
index 7c3a183f6..0c2622fe3 100644
--- a/apps/mobile/src/morph/hono.ts
+++ b/apps/mobile/src/morph/hono.ts
@@ -82,7 +82,7 @@ class Morph {
return { subscriptions, ...collections }
}
- toList({ list: data }: HonoApiClient.List_Get): ListModel {
+ toList(data: HonoApiClient.List_Get["list"] | HonoApiClient.List_List_Get): ListModel {
return {
id: data.id,
title: data.title!,
diff --git a/apps/mobile/src/morph/types.ts b/apps/mobile/src/morph/types.ts
index 1112477cc..531537028 100644
--- a/apps/mobile/src/morph/types.ts
+++ b/apps/mobile/src/morph/types.ts
@@ -10,4 +10,5 @@ export namespace HonoApiClient {
export type List_Get = ExtractData
export type Entry_Post = ExtractData
export type Entry_Get = ExtractData
+ export type List_List_Get = ExtractData[number]
}
diff --git a/apps/mobile/src/store/list/store.ts b/apps/mobile/src/store/list/store.ts
index 35563b1ab..674a3f5b6 100644
--- a/apps/mobile/src/store/list/store.ts
+++ b/apps/mobile/src/store/list/store.ts
@@ -84,10 +84,17 @@ class ListSyncServices {
async fetchListById(params: { id: string }) {
const list = await apiClient.lists.$get({ query: { listId: params.id } })
- listActions.upsertMany([honoMorph.toList(list.data)])
+ listActions.upsertMany([honoMorph.toList(list.data.list)])
return list.data
}
+
+ async fetchOwnedLists() {
+ const res = await apiClient.lists.list.$get()
+ listActions.upsertMany(res.data.map((list) => honoMorph.toList(list)))
+
+ return res.data
+ }
}
export const listSyncServices = new ListSyncServices()
diff --git a/icons/mgc/user_add_2_cute_fi.svg b/icons/mgc/user_add_2_cute_fi.svg
new file mode 100644
index 000000000..dec029af7
--- /dev/null
+++ b/icons/mgc/user_add_2_cute_fi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/wallet_2_cute_fi.svg b/icons/mgc/wallet_2_cute_fi.svg
new file mode 100644
index 000000000..1627fb013
--- /dev/null
+++ b/icons/mgc/wallet_2_cute_fi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 480463a2d..18fb073fd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -436,6 +436,9 @@ importers:
'@follow/utils':
specifier: workspace:*
version: link:../../packages/utils
+ '@formatjs/intl-numberformat':
+ specifier: 8.15.2
+ version: 8.15.2
'@gorhom/portal':
specifier: 1.0.14
version: 1.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
@@ -475,6 +478,9 @@ importers:
dayjs:
specifier: 1.11.13
version: 1.11.13
+ dnum:
+ specifier: ^2.14.0
+ version: 2.14.0
es-toolkit:
specifier: 1.29.0
version: 1.29.0
@@ -3777,6 +3783,18 @@ packages:
'@fontsource/sn-pro@5.1.0':
resolution: {integrity: sha512-k7cdU1hftD/pyrnrmQg+egKAssbuPORbtgQM/9JG5b76EchEKZdl/juO8xBjN3O/H5BQ16iu8/9vNPgzyExZeg==}
+ '@formatjs/ecma402-abstract@2.3.2':
+ resolution: {integrity: sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==}
+
+ '@formatjs/fast-memoize@2.2.6':
+ resolution: {integrity: sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==}
+
+ '@formatjs/intl-localematcher@0.5.10':
+ resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
+
+ '@formatjs/intl-numberformat@8.15.2':
+ resolution: {integrity: sha512-8rdXSBKXb0nyfEX98/Sh3+ML4JXK2vm7Slnrjikx1svXoBKhiw8kZKwQMpAcGIKtEiQA0conUW3LMPXzv/YlSg==}
+
'@freakycoder/react-native-bounceable@1.0.3':
resolution: {integrity: sha512-+iMq2tnqxCwFjitbPUz9nZ+VfJ8OU9waIlDJAAsoq1229QEwCmERCNy5zVtDsz75q3i4FLXX/n7fimdMzmP21A==}
@@ -18338,6 +18356,28 @@ snapshots:
'@fontsource/sn-pro@5.1.0': {}
+ '@formatjs/ecma402-abstract@2.3.2':
+ dependencies:
+ '@formatjs/fast-memoize': 2.2.6
+ '@formatjs/intl-localematcher': 0.5.10
+ decimal.js: 10.4.3
+ tslib: 2.8.1
+
+ '@formatjs/fast-memoize@2.2.6':
+ dependencies:
+ tslib: 2.8.1
+
+ '@formatjs/intl-localematcher@0.5.10':
+ dependencies:
+ tslib: 2.8.1
+
+ '@formatjs/intl-numberformat@8.15.2':
+ dependencies:
+ '@formatjs/ecma402-abstract': 2.3.2
+ '@formatjs/intl-localematcher': 0.5.10
+ decimal.js: 10.4.3
+ tslib: 2.8.1
+
'@freakycoder/react-native-bounceable@1.0.3': {}
'@gar/promisify@1.1.3': {}
@@ -23274,8 +23314,7 @@ snapshots:
optionalDependencies:
supports-color: 8.1.1
- decimal.js@10.4.3:
- optional: true
+ decimal.js@10.4.3: {}
decode-bmp@0.2.1:
dependencies: