Skip to content

Commit

Permalink
Merge branch 'dev' into feat/mobile-masonry-grid
Browse files Browse the repository at this point in the history
  • Loading branch information
hyoban authored Jan 21, 2025
2 parents 9281ec0 + 166e095 commit 40c2fbe
Show file tree
Hide file tree
Showing 64 changed files with 1,382 additions and 371 deletions.
1 change: 1 addition & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
favicon: iconPath,
},
plugins: [
"expo-localization",
[
"expo-router",
{
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { DOMProps } from "expo/dom"
import type { FC } from "react"
import type WebView from "react-native-webview"

declare global {
export type WebComponent<P = object> = FC<P & { dom?: DOMProps }>
export type WebComponent<P = object> = FC<P & { dom?: DOMProps } & React.RefAttributes<WebView>>
}
export {}
5 changes: 3 additions & 2 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@follow/mobile",
"version": "1.0.0",
"version": "0.1.0",
"private": true,
"main": "src/main.tsx",
"scripts": {
Expand Down Expand Up @@ -43,14 +43,15 @@
"expo-blur": "~14.0.1",
"expo-build-properties": "^0.13.1",
"expo-clipboard": "~7.0.0",
"expo-constants": "~17.0.3",
"expo-constants": "~17.0.4",
"expo-dev-client": "^5.0.9",
"expo-file-system": "~18.0.6",
"expo-font": "~13.0.1",
"expo-haptics": "~14.0.0",
"expo-image": "~2.0.3",
"expo-linear-gradient": "~14.0.1",
"expo-linking": "~7.0.3",
"expo-localization": "~16.0.1",
"expo-router": "4.0.11",
"expo-secure-store": "^14.0.1",
"expo-sharing": "~13.0.0",
Expand Down
44 changes: 44 additions & 0 deletions apps/mobile/src/atoms/settings/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { GeneralSettings } from "@/src/interfaces/settings/general"

import { createSettingAtom } from "./internal/helper"

const createDefaultSettings = (): GeneralSettings => ({
// App

language: "en",
translationLanguage: "zh-CN",

// Data control

sendAnonymousData: true,

autoGroup: true,

// view
unreadOnly: true,
// mark unread
scrollMarkUnread: true,

renderMarkUnread: false,
// UX
groupByDate: true,
autoExpandLongSocialMedia: false,

// Secure
jumpOutLinkWarn: true,
// TTS
voice: "en-US-AndrewMultilingualNeural",
})

export const {
useSettingKey: useGeneralSettingKey,
useSettingSelector: useGeneralSettingSelector,
useSettingKeys: useGeneralSettingKeys,
setSetting: setGeneralSetting,
clearSettings: clearGeneralSettings,
initializeDefaultSettings: initializeDefaultGeneralSettings,
getSettings: getGeneralSettings,
useSettingValue: useGeneralSettingValue,

settingAtom: __generalSettingAtom,
} = createSettingAtom("general", createDefaultSettings)
160 changes: 160 additions & 0 deletions apps/mobile/src/atoms/settings/internal/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useRefValue } from "@follow/hooks"
import { createAtomHooks } from "@follow/utils"
import type { SetStateAction, WritableAtom } from "jotai"
import { atom as jotaiAtom, useAtomValue } from "jotai"
import { atomWithStorage, selectAtom } from "jotai/utils"
import { useMemo } from "react"
import { shallow } from "zustand/shallow"

import { JotaiPersistSyncStorage } from "@/src/lib/jotai"

const getStorageNS = (settingKey: string) => `follow-rn-${settingKey}`
type Nullable<T> = T | null | undefined

export const createSettingAtom = <T extends object>(
settingKey: string,
createDefaultSettings: () => T,
) => {
const atom = atomWithStorage(
getStorageNS(settingKey),
createDefaultSettings(),
JotaiPersistSyncStorage,
{
getOnInit: true,
},
) as WritableAtom<T, [SetStateAction<T>], void>

const [, , useSettingValue, , getSettings, setSettings] = createAtomHooks(atom)

const initializeDefaultSettings = () => {
const currentSettings = getSettings()
const defaultSettings = createDefaultSettings()
if (typeof currentSettings !== "object") setSettings(defaultSettings)
const newSettings = { ...defaultSettings, ...currentSettings }
setSettings(newSettings)
}

const selectAtomCacheMap = {} as Record<keyof ReturnType<typeof getSettings>, any>

const noopAtom = jotaiAtom(null)

const useMaybeSettingKey = <T extends keyof ReturnType<typeof getSettings>>(key: Nullable<T>) => {
// @ts-expect-error
let selectedAtom: Record<keyof T, any>[T] | null = null
if (key) {
selectedAtom = selectAtomCacheMap[key]
if (!selectedAtom) {
selectedAtom = selectAtom(atom, (s) => s[key])
selectAtomCacheMap[key] = selectedAtom
}
} else {
selectedAtom = noopAtom
}

return useAtomValue(selectedAtom) as ReturnType<typeof getSettings>[T]
}

const useSettingKey = <T extends keyof ReturnType<typeof getSettings>>(key: T) => {
return useMaybeSettingKey(key) as ReturnType<typeof getSettings>[T]
}

function useSettingKeys<
T extends keyof ReturnType<typeof getSettings>,
K1 extends T,
K2 extends T,
K3 extends T,
K4 extends T,
K5 extends T,
K6 extends T,
K7 extends T,
K8 extends T,
K9 extends T,
K10 extends T,
>(keys: [K1, K2?, K3?, K4?, K5?, K6?, K7?, K8?, K9?, K10?]) {
return [
useMaybeSettingKey(keys[0]),
useMaybeSettingKey(keys[1]),
useMaybeSettingKey(keys[2]),
useMaybeSettingKey(keys[3]),
useMaybeSettingKey(keys[4]),
useMaybeSettingKey(keys[5]),
useMaybeSettingKey(keys[6]),
useMaybeSettingKey(keys[7]),
useMaybeSettingKey(keys[8]),
useMaybeSettingKey(keys[9]),
] as [
ReturnType<typeof getSettings>[K1],
ReturnType<typeof getSettings>[K2],
ReturnType<typeof getSettings>[K3],
ReturnType<typeof getSettings>[K4],
ReturnType<typeof getSettings>[K5],
ReturnType<typeof getSettings>[K6],
ReturnType<typeof getSettings>[K7],
ReturnType<typeof getSettings>[K8],
ReturnType<typeof getSettings>[K9],
ReturnType<typeof getSettings>[K10],
]
}

const useSettingSelector = <
T extends keyof ReturnType<typeof getSettings>,
S extends ReturnType<typeof getSettings>,
R = S[T],
>(
selector: (s: S) => R,
): R => {
const stableSelector = useRefValue(selector)

return useAtomValue(
// @ts-expect-error
useMemo(() => selectAtom(atom, stableSelector.current, shallow), [stableSelector]),
)
}

const setSetting = <K extends keyof ReturnType<typeof getSettings>>(
key: K,
value: ReturnType<typeof getSettings>[K],
) => {
const updated = Date.now()
setSettings({
...getSettings(),
[key]: value,

updated,
})
}

const clearSettings = () => {
setSettings(createDefaultSettings())
}

Object.defineProperty(useSettingValue, "select", {
value: useSettingSelector,
})

return {
useSettingKey,
useSettingSelector,
setSetting,
clearSettings,
initializeDefaultSettings,

useSettingValue,
useSettingKeys,
getSettings,

settingAtom: atom,
} as {
useSettingKey: typeof useSettingKey
useSettingSelector: typeof useSettingSelector
setSetting: typeof setSetting
clearSettings: typeof clearSettings
initializeDefaultSettings: typeof initializeDefaultSettings
useSettingValue: typeof useSettingValue & {
select: <T extends keyof ReturnType<() => T>>(key: T) => Awaited<T[T]>
}
useSettingKeys: typeof useSettingKeys
getSettings: typeof getSettings
settingAtom: typeof atom
}
}
34 changes: 34 additions & 0 deletions apps/mobile/src/components/common/BlurEffect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { StyleSheet } from "react-native"
import { useColor } from "react-native-uikit-colors"

import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"

const node = (
<ThemedBlurView
style={{

Check warning on line 8 in apps/mobile/src/components/common/BlurEffect.tsx

View workflow job for this annotation

GitHub Actions / auto-fix

Inline style: { overflow: 'hidden', backgroundColor: 'transparent' }

Check warning on line 8 in apps/mobile/src/components/common/BlurEffect.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint and Typecheck (lts/*)

Inline style: { overflow: 'hidden', backgroundColor: 'transparent' }
...StyleSheet.absoluteFillObject,
overflow: "hidden",
backgroundColor: "transparent",
}}
/>
)
export const BlurEffect = () => {
return node
}

const InternalBlurEffectWithBottomBorder = () => {
const border = useColor("opaqueSeparator")
return (
<ThemedBlurView
style={{

Check warning on line 23 in apps/mobile/src/components/common/BlurEffect.tsx

View workflow job for this annotation

GitHub Actions / auto-fix

Inline style: { overflow: 'hidden', backgroundColor: 'transparent' }

Check warning on line 23 in apps/mobile/src/components/common/BlurEffect.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint and Typecheck (lts/*)

Inline style: { overflow: 'hidden', backgroundColor: 'transparent' }
...StyleSheet.absoluteFillObject,
overflow: "hidden",
backgroundColor: "transparent",
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: border,
}}
/>
)
}

export const BlurEffectWithBottomBorder = () => <InternalBlurEffectWithBottomBorder />
33 changes: 2 additions & 31 deletions apps/mobile/src/components/common/FollowWebView.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { callWebviewExpose } from "@follow/shared"
import { parseSafeUrl, transformVideoUrl } from "@follow/utils"
import * as Linking from "expo-linking"
import type { RefObject } from "react"
import { useCallback, useEffect, useState } from "react"
import { Platform } from "react-native"
import type { WebViewNavigation, WebViewProps } from "react-native-webview"
import type { WebViewProps } from "react-native-webview"
import { WebView } from "react-native-webview"

import { useWebViewNavigation } from "@/src/hooks/useWebViewNavigation"
import { signOut } from "@/src/lib/auth"
import { useOpenLink } from "@/src/lib/hooks/use-open-link"

const presetUri = Platform.select({
ios: "rn-web/index.html",
android: "file:///android_asset/raw/index.html",
default: "https://app.follow.is",
})

const allowHosts = new Set(["app.follow.is"])

interface FollowWebViewProps extends WebViewProps {
customUrl?: string
}
Expand Down Expand Up @@ -80,32 +77,6 @@ export const FollowWebView = ({
)
}

const useWebViewNavigation = ({ webViewRef }: { webViewRef: RefObject<WebView> }) => {
const openLink = useOpenLink()

const onNavigationStateChange = useCallback(
(newNavState: WebViewNavigation) => {
const { url: urlStr } = newNavState
const url = parseSafeUrl(urlStr)
if (!url) return
if (url.protocol === "file:") return
if (allowHosts.has(url.host)) return

webViewRef.current?.stopLoading()

const formattedUrl = transformVideoUrl({ url: urlStr })
if (formattedUrl) {
openLink(formattedUrl)
return
}
openLink(urlStr)
},
[openLink, webViewRef],
)

return { onNavigationStateChange }
}

// We only need to handle deep link at the first time the app is opened
let lastInitialUrl: string | null = null
const useDeepLink = ({
Expand Down
16 changes: 0 additions & 16 deletions apps/mobile/src/components/common/HeaderBlur.tsx

This file was deleted.

Loading

0 comments on commit 40c2fbe

Please sign in to comment.