Skip to content

Commit

Permalink
chore: Release v0.3.1-beta.0 (#2515)
Browse files Browse the repository at this point in the history
  • Loading branch information
DIYgod authored Jan 10, 2025
2 parents 871dbf3 + 6a88f05 commit 9f9ca45
Show file tree
Hide file tree
Showing 166 changed files with 8,656 additions and 1,728 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
["[a-zA-Z]+[cC]lass[nN]ame[\"'`]?:\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"],
["[a-zA-Z]+[cC]lass[nN]ame\\s*=\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"]
],
"tailwindCSS.experimental.configFile": {
"apps/mobile/tailwind.config.ts": "apps/mobile/**",
"apps/server/tailwind.config.ts": "apps/server/**",
"tailwind.config.ts": ["!apps/mobile/**", "!apps/server/**", "**"]
},
"typescript.tsserver.maxTsServerMemory": 8096,
"typescript.tsserver.nodePath": "node",
// If you do not want to autofix some rules on save
Expand Down
38 changes: 37 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

File renamed without changes.
1 change: 1 addition & 0 deletions apps/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@openpanel/web": "1.0.1",
"@sentry/electron": "5.7.0",
"builder-util-runtime": "9.2.10",
"chardet": "^2.0.0",
"cookie-es": "^1.2.2",
"dompurify": "~3.2.2",
"electron-context-menu": "4.0.4",
Expand Down
2 changes: 1 addition & 1 deletion apps/main/src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const getTrayIconPath = () => {
}
if (isWindows) {
// https://www.electronjs.org/docs/latest/api/tray#:~:text=Windows,best%20visual%20effects.
return path.join(__dirname, "../../resources/icon.ico")
return path.join(__dirname, "../../resources/icon-no-padding.ico")
}
return getIconPath()
}
35 changes: 23 additions & 12 deletions apps/main/src/lib/readability.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Readability } from "@mozilla/readability"
import { name, version } from "@pkg"
import chardet from "chardet"
import DOMPurify from "dompurify"
import { parseHTML } from "linkedom"
import { fetch } from "ofetch"
Expand All @@ -21,24 +22,34 @@ function sanitizeHTMLString(dirtyDocumentString: string) {
return sanitizedDocumentString
}

/**
* Decodes the response body of a `fetch` request into a string, ensuring proper character set handling.
* @throws Will return "Failed to decode response content." if the decoding process encounters any errors.
*/
async function decodeResponseBodyChars(res: Response) {
// Read the response body as an ArrayBuffer
const buffer = await res.arrayBuffer()
// Step 1: Get charset from Content-Type header
const contentType = res.headers.get("content-type")
const httpCharset = contentType?.match(/charset=([\w-]+)/i)?.[1]
// Step 2: Use charset from Content-Type header or fall back to chardet
const detectedCharset = httpCharset || chardet.detect(Buffer.from(buffer)) || "utf-8"
// Step 3: Decode the response body using the detected charset
try {
const decodedText = new TextDecoder(detectedCharset, { fatal: false }).decode(buffer)
return decodedText
} catch {
return "Failed to decode response content."
}
}

export async function readability(url: string) {
const dirtyDocumentString = await fetch(url, {
headers: {
"User-Agent": userAgents,
Accept: "text/html",
},
}).then(async (res) => {
const contentType = res.headers.get("content-type")
// text/html; charset=GBK
if (!contentType) return res.text()
const charset = contentType.match(/charset=([a-zA-Z-\d]+)/)?.[1]
if (charset) {
const blob = await res.blob()
const buffer = await blob.arrayBuffer()
return new TextDecoder(charset).decode(buffer)
}
return res.text()
})
}).then(decodeResponseBodyChars)

const sanitizedDocumentString = sanitizeHTMLString(dirtyDocumentString)
const baseUrl = new URL(url).origin
Expand Down
2 changes: 1 addition & 1 deletion apps/main/src/lib/tray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const registerAppTray = () => {

const icon = nativeImage.createFromPath(getTrayIconPath())
// See https://stackoverflow.com/questions/41664208/electron-tray-icon-change-depending-on-dark-theme/41998326#41998326
const trayIcon = icon.resize({ width: 16 })
const trayIcon = isMacOS ? icon.resize({ width: 16 }) : icon
trayIcon.setTemplateImage(true)
tray = new Tray(trayIcon)

Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@follow/mobile",
"version": "1.0.0",
"private": true,
"main": "src/main.ts",
"main": "src/main.tsx",
"scripts": {
"android": "expo run:android",
"db:generate": "drizzle-kit generate",
Expand Down Expand Up @@ -43,6 +43,7 @@
"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-router": "4.0.11",
Expand All @@ -57,6 +58,7 @@
"hono": "4.6.13",
"immer": "10.1.1",
"jotai": "2.10.3",
"nanoid": "5.0.9",
"nativewind": "4.1.23",
"ofetch": "1.4.1",
"react": "^18.3.1",
Expand All @@ -68,6 +70,7 @@
"react-native-keyboard-controller": "^1.15.0",
"react-native-pager-view": "6.6.1",
"react-native-reanimated": "~3.16.5",
"react-native-root-siblings": "5.0.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
"react-native-svg": "15.8.0",
Expand Down
17 changes: 17 additions & 0 deletions apps/mobile/src/atoms/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { atom } from "jotai"

export const loadingVisibleAtom = atom(false)

export const loadingAtom = atom<{
finish?: null | (() => any)
cancel?: null | (() => any)
error?: null | ((err: any) => any)
done?: null | ((r: unknown) => any)
thenable: null | Promise<any>
}>({
finish: null,
cancel: null,
error: null,
done: null,
thenable: null,
})
45 changes: 28 additions & 17 deletions apps/mobile/src/components/common/FollowWebView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,24 +117,35 @@ const useDeepLink = ({
}) => {
const handleDeepLink = useCallback(
async (url: string) => {
const { queryParams } = Linking.parse(url)
if (!queryParams) {
console.error("Invalid URL! queryParams is not available", url)
return
}
const id = queryParams["id"] ?? undefined
const isList = queryParams["type"] === "list"
// const urlParam = queryParams["url"] ?? undefined
if (!id || typeof id !== "string") {
console.error("Invalid URL! id is not a string", url)
return
}
const injectJavaScript = webViewRef.current?.injectJavaScript
if (!injectJavaScript) {
console.error("injectJavaScript is not available")
return
const { queryParams, path, hostname } = Linking.parse(url)

const pathname = (hostname || "") + (path || "")
const pathnameTrimmed = pathname?.endsWith("/") ? pathname.slice(0, -1) : pathname

switch (pathnameTrimmed) {
case "/add":
case "/follow": {
if (!queryParams) {
console.error("Invalid URL! queryParams is not available", url)
return
}

const id = queryParams["id"] ?? undefined
const isList = queryParams["type"] === "list"
// const urlParam = queryParams["url"] ?? undefined
if (!id || typeof id !== "string") {
console.error("Invalid URL! id is not a string", url)
return
}
const injectJavaScript = webViewRef.current?.injectJavaScript
if (!injectJavaScript) {
console.error("injectJavaScript is not available")
return
}
callWebviewExpose(injectJavaScript).follow({ id, isList })
return
}
}
callWebviewExpose(injectJavaScript).follow({ id, isList })
},
[webViewRef],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FullWindowOverlay } from "react-native-screens"
1 change: 1 addition & 0 deletions apps/mobile/src/components/common/FullWindowOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Fragment as FullWindowOverlay } from "react"
61 changes: 61 additions & 0 deletions apps/mobile/src/components/common/HeaderTitleExtra.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { cn } from "@follow/utils"
import { useTheme } from "@react-navigation/native"
import type { StyleProp, TextProps, TextStyle } from "react-native"
import { Animated, Platform, StyleSheet, Text, View } from "react-native"

type Props = Omit<TextProps, "style"> & {
tintColor?: string
children?: string
style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>
subText?: string
subTextStyle?: StyleProp<TextStyle>
subTextClassName?: string
}

export function HeaderTitleExtra({
tintColor,
style,
subText,
subTextStyle,
subTextClassName,
...rest
}: Props) {
const { colors, fonts } = useTheme()

return (
<View>
<Animated.Text
accessibilityRole="header"
aria-level="1"
numberOfLines={1}
{...rest}
style={[
{ color: tintColor === undefined ? colors.text : tintColor },
Platform.select({ ios: fonts.bold, default: fonts.medium }),
styles.title,
style,
]}
/>
<Text
className={cn("text-text/50 text-center text-xs", subTextClassName)}

Check warning on line 40 in apps/mobile/src/components/common/HeaderTitleExtra.tsx

View workflow job for this annotation

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

Classname 'text-text/50' is not a Tailwind CSS class!
style={subTextStyle}
>
{subText}
</Text>
</View>
)
}

const styles = StyleSheet.create({
title: Platform.select({
ios: {
fontSize: 17,
},
android: {
fontSize: 20,
},
default: {
fontSize: 18,
},
}),
})
45 changes: 44 additions & 1 deletion apps/mobile/src/components/common/ModalSharedComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,61 @@
import { withOpacity } from "@follow/utils"
import { router } from "expo-router"
import { TouchableOpacity } from "react-native"

import { useIsRouteOnlyOne } from "@/src/hooks/useIsRouteOnlyOne"
import { CheckLineIcon } from "@/src/icons/check_line"
import { CloseCuteReIcon } from "@/src/icons/close_cute_re"
import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
import { useColor } from "@/src/theme/colors"

import { RotateableLoading } from "./RotateableLoading"

export const ModalHeaderCloseButton = () => {
return <ModalHeaderCloseButtonImpl />
}

const ModalHeaderCloseButtonImpl = () => {
const label = useColor("label")

const routeOnlyOne = useIsRouteOnlyOne()

return (
<TouchableOpacity onPress={() => router.dismiss()}>
<CloseCuteReIcon color={label} />
{routeOnlyOne ? (
<CloseCuteReIcon height={20} width={20} color={label} />
) : (
<MingcuteLeftLineIcon height={20} width={20} color={label} />
)}
</TouchableOpacity>
)
}

export interface ModalHeaderShubmitButtonProps {
isValid: boolean
onPress: () => void
isLoading?: boolean
}
export const ModalHeaderShubmitButton = ({
isValid,
onPress,
isLoading,
}: ModalHeaderShubmitButtonProps) => {
return <ModalHeaderShubmitButtonImpl isValid={isValid} onPress={onPress} isLoading={isLoading} />
}

const ModalHeaderShubmitButtonImpl = ({
isValid,
onPress,
isLoading,
}: ModalHeaderShubmitButtonProps) => {
const label = useColor("label")
return (
<TouchableOpacity onPress={onPress} disabled={!isValid || isLoading}>
{isLoading ? (
<RotateableLoading size={20} color={withOpacity(label, 0.5)} />
) : (
<CheckLineIcon height={20} width={20} color={isValid ? label : withOpacity(label, 0.5)} />
)}
</TouchableOpacity>
)
}
39 changes: 39 additions & 0 deletions apps/mobile/src/components/common/RotateableLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { FC } from "react"
import { useEffect } from "react"
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated"

import { Loading3CuteReIcon } from "@/src/icons/loading_3_cute_re"

export interface RotateableLoadingProps {
size?: number
color?: string
}
export const RotateableLoading: FC<RotateableLoadingProps> = ({ size = 36, color = "#fff" }) => {
const rotate = useSharedValue(0)
useEffect(() => {
rotate.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
Infinity,
false,
)
return () => {
rotate.value = 0
}
}, [rotate])

const rotateStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotate.value}deg` }],
}))

return (
<Animated.View style={rotateStyle}>
<Loading3CuteReIcon height={size} width={size} color={color} />
</Animated.View>
)
}
19 changes: 19 additions & 0 deletions apps/mobile/src/components/ui/form/FormProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createContext, useContext } from "react"
import type { FieldValues, UseFormReturn } from "react-hook-form"

const FormContext = createContext<UseFormReturn<any> | null>(null)

export function FormProvider<T extends FieldValues>(props: {
form: UseFormReturn<T>
children: React.ReactNode
}) {
return <FormContext.Provider value={props.form}>{props.children}</FormContext.Provider>
}

export function useFormContext<T extends FieldValues>() {

Check warning on line 13 in apps/mobile/src/components/ui/form/FormProvider.tsx

View workflow job for this annotation

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

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(FormContext)
if (!context) {
throw new Error("useFormContext must be used within a FormProvider")
}
return context as UseFormReturn<T>
}
Loading

0 comments on commit 9f9ca45

Please sign in to comment.