diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..bdbe5864 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,46 @@ +## What does this PR do? + + + +- Fixes #XXXX (GitHub issue number) + +## Visual Demo + +A visual demonstration is strongly recommended, for both the original and new change **(video / image)**. + +#### Video Demo (if applicable): + +- Show screen recordings of the issue or feature. +- Demonstrate how to reproduce the issue, the behavior before and after the change. + +#### Image Demo (if applicable): + +- Add side-by-side screenshots of the original and updated change. +- Highlight any significant change(s). + +## AI Assistance (Required) + +We allow AI-assisted development, but reviewers need transparency to assess risk, maintainability, and correctness. + +#### AI usage level (check one) + +- [ ] None (no AI used) +- [ ] Light (spellcheck/rewording/comments/docs only) +- [ ] Medium (AI suggested small code changes/snippets that I adapted) +- [ ] Heavy (AI significantly shaped the implementation or architecture) + +#### Which tool(s) where used? + +- e.g., ChatGPT, Copilot, Cursor, etc. + +## What was verified by the author? + + + +- [ ] I reviewed **and** understood all AI/human generated code +- [ ] I validated behavior locally (tests/manual verification) +- [ ] I checked edge cases and failure modes + +## Submission Acknowledgement + +- [ ] I acknowledge that a decent size PR without self-review might be rejected diff --git a/apps/mobile/app.json b/apps/mobile/app.json index b4ea6c42..f25ec1cd 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Linkwarden", "slug": "linkwarden", - "version": "1.0.0", + "version": "1.0.1", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "linkwarden", diff --git a/apps/mobile/app/(tabs)/collections/index.tsx b/apps/mobile/app/(tabs)/collections/index.tsx index 36c082f6..f13a58a0 100644 --- a/apps/mobile/app/(tabs)/collections/index.tsx +++ b/apps/mobile/app/(tabs)/collections/index.tsx @@ -44,7 +44,7 @@ export default function CollectionsScreen() { collapsableChildren={false} > {collections.isLoading ? ( - + Loading... diff --git a/apps/mobile/app/(tabs)/dashboard/_layout.tsx b/apps/mobile/app/(tabs)/dashboard/_layout.tsx index 72a0407d..c129eafc 100644 --- a/apps/mobile/app/(tabs)/dashboard/_layout.tsx +++ b/apps/mobile/app/(tabs)/dashboard/_layout.tsx @@ -27,8 +27,8 @@ export default function Layout() { Platform.OS === "ios" ? "transparent" : colorScheme === "dark" - ? rawTheme["dark"]["base-100"] - : "white", + ? rawTheme["dark"]["base-100"] + : "white", }, }} > diff --git a/apps/mobile/app/(tabs)/dashboard/index.tsx b/apps/mobile/app/(tabs)/dashboard/index.tsx index ffb5ff56..19f734c1 100644 --- a/apps/mobile/app/(tabs)/dashboard/index.tsx +++ b/apps/mobile/app/(tabs)/dashboard/index.tsx @@ -1,4 +1,11 @@ -import { Platform, ScrollView, StyleSheet } from "react-native"; +import { + ActivityIndicator, + Platform, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; import React, { useEffect, useMemo, useState } from "react"; import { useDashboardData } from "@linkwarden/router/dashboardData"; import useAuthStore from "@/store/auth"; @@ -53,22 +60,36 @@ export default function DashboardScreen() { }); }, [dashboardSections]); + const [pullRefreshing, setPullRefreshing] = useState(false); + + const onRefresh = async () => { + setPullRefreshing(true); + try { + await Promise.all([ + dashboardData.refetch(), + userData.refetch(), + collectionsData.refetch(), + tagsData.refetch(), + ]); + } finally { + setPullRefreshing(false); + } + }; + + if (orderedSections.length === 0 && dashboardData.isLoading) + return ( + + + Loading... + + ); + return ( { - dashboardData.refetch(); - userData.refetch(); - collectionsData.refetch(); - tagsData.refetch(); - }} + refreshing={pullRefreshing} + onRefresh={onRefresh} progressBackgroundColor={ rawTheme[colorScheme as ThemeName]["base-200"] } diff --git a/apps/mobile/app/(tabs)/tags/index.tsx b/apps/mobile/app/(tabs)/tags/index.tsx index ff20d7d6..12bceaf7 100644 --- a/apps/mobile/app/(tabs)/tags/index.tsx +++ b/apps/mobile/app/(tabs)/tags/index.tsx @@ -42,7 +42,7 @@ export default function TagsScreen() { collapsableChildren={false} > {tags.isLoading ? ( - + Loading... diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx index acdd2eee..0a80a483 100644 --- a/apps/mobile/app/index.tsx +++ b/apps/mobile/app/index.tsx @@ -20,56 +20,58 @@ export default function HomeScreen() { return ( - - - - Linkwarden - - - - - Welcome to the official mobile app for Linkwarden! - + + + + + Linkwarden + + + + + Welcome to the official mobile app for Linkwarden! + - - Expect regular improvements and new features as we continue refining - the experience. - + + Expect regular improvements and new features as we continue refining + the experience. + + + + + + + + SheetManager.show("support-sheet")} + > + Need help? + + - - - - - - SheetManager.show("support-sheet")} - > - Need help? - - ); } diff --git a/apps/mobile/components/ActionSheets/AddLinkSheet.tsx b/apps/mobile/components/ActionSheets/AddLinkSheet.tsx index 12b33731..c45e80f3 100644 --- a/apps/mobile/components/ActionSheets/AddLinkSheet.tsx +++ b/apps/mobile/components/ActionSheets/AddLinkSheet.tsx @@ -1,4 +1,4 @@ -import { Alert, Platform, Text, View } from "react-native"; +import { Alert, Text, View } from "react-native"; import { useRef, useState } from "react"; import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet"; import Input from "@/components/ui/Input"; diff --git a/apps/mobile/components/ActionSheets/EditLinkSheet.tsx b/apps/mobile/components/ActionSheets/EditLinkSheet.tsx index c7d82938..531ed6ca 100644 --- a/apps/mobile/components/ActionSheets/EditLinkSheet.tsx +++ b/apps/mobile/components/ActionSheets/EditLinkSheet.tsx @@ -1,4 +1,4 @@ -import { View, Text, Alert, Platform } from "react-native"; +import { View, Text, Alert } from "react-native"; import { useCallback, useEffect, useMemo, useState } from "react"; import ActionSheet, { FlatList, diff --git a/apps/mobile/components/ActionSheets/NewCollectionSheet.tsx b/apps/mobile/components/ActionSheets/NewCollectionSheet.tsx index 32aa3070..3a65f54c 100644 --- a/apps/mobile/components/ActionSheets/NewCollectionSheet.tsx +++ b/apps/mobile/components/ActionSheets/NewCollectionSheet.tsx @@ -1,4 +1,4 @@ -import { Alert, Platform, Text, View } from "react-native"; +import { Alert, Text, View } from "react-native"; import { useRef, useState } from "react"; import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet"; import Input from "@/components/ui/Input"; diff --git a/apps/mobile/components/Links.tsx b/apps/mobile/components/Links.tsx index 8c7f6785..88108796 100644 --- a/apps/mobile/components/Links.tsx +++ b/apps/mobile/components/Links.tsx @@ -28,7 +28,7 @@ export default function Links({ links, data }: Props) { const [promptedRefetch, setPromptedRefetch] = useState(false); return data.isLoading ? ( - + Loading... diff --git a/apps/mobile/components/Modals/AddLink.tsx b/apps/mobile/components/Modals/AddLink.tsx deleted file mode 100644 index c2115fc7..00000000 --- a/apps/mobile/components/Modals/AddLink.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PropsWithChildren } from "react"; -import { IconSymbol } from "../ui/IconSymbol"; -import ModalBase from "../ModalBase"; -import { Text } from "react-native"; - -type Props = PropsWithChildren<{ - isVisible: boolean; - onClose: () => void; -}>; - -export default function AddLink({ isVisible, onClose }: Props) { - return ( - // - Hi - // - ); -} diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json index c8f1025d..036f2f84 100644 --- a/apps/mobile/eas.json +++ b/apps/mobile/eas.json @@ -20,6 +20,7 @@ } }, "production": { + "corepack": true, "distribution": "store", "autoIncrement": true, "channel": "production" diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 3a55e4ed..20133b9d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,7 +1,7 @@ { "name": "@linkwarden/mobile", "main": "expo-router/entry", - "version": "1.0.0", + "version": "0.0.0", "scripts": { "start": "expo start", "android": "expo run:android", diff --git a/apps/mobile/store/auth.ts b/apps/mobile/store/auth.ts index b04178f2..c2e7f760 100644 --- a/apps/mobile/store/auth.ts +++ b/apps/mobile/store/auth.ts @@ -3,13 +3,10 @@ import * as SecureStore from "expo-secure-store"; import { router } from "expo-router"; import { MobileAuth } from "@linkwarden/types"; import { Alert } from "react-native"; -import * as FileSystem from "expo-file-system"; import { queryClient } from "@/lib/queryClient"; import { mmkvPersister } from "@/lib/queryPersister"; import { clearCache } from "@/lib/cache"; -const CACHE_DIR = FileSystem.documentDirectory + "archivedData/"; - type AuthStore = { auth: MobileAuth; signIn: ( @@ -78,31 +75,42 @@ const useAuthStore = create((set) => ({ } }); } else { - await fetch(instance + "/api/v1/session", { - method: "POST", - body: JSON.stringify({ username, password }), - headers: { - "Content-Type": "application/json", - }, - }).then(async (res) => { + try { + const res = await Promise.race([ + fetch(`${instance}/api/v1/session`, { + method: "POST", + body: JSON.stringify({ username, password }), + headers: { "Content-Type": "application/json" }, + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error("TIMEOUT")), 30000) + ), + ]); + if (res.ok) { const data = await res.json(); const session = (data as any).response.token; + await SecureStore.setItemAsync("TOKEN", session); await SecureStore.setItemAsync("INSTANCE", instance); - set({ - auth: { - session, - instance, - status: "authenticated", - }, - }); - + set({ auth: { session, instance, status: "authenticated" } }); router.replace("/(tabs)/dashboard"); } else { Alert.alert("Error", "Invalid credentials"); } - }); + } catch (err: any) { + if (err?.message === "TIMEOUT") { + Alert.alert( + "Request timed out", + "Unable to reach the server in time. Please check your network configuration and try again." + ); + } else { + Alert.alert( + "Network error", + "Could not connect to the server. Please check your network configuration and try again." + ); + } + } } }, signOut: async () => { diff --git a/apps/mobile/store/data.ts b/apps/mobile/store/data.ts index b3a59065..c9ba3ac0 100644 --- a/apps/mobile/store/data.ts +++ b/apps/mobile/store/data.ts @@ -15,13 +15,13 @@ const useDataStore = create((set, get) => ({ hasShareIntent: false, url: "", }, - theme: "light", + theme: "system", preferredBrowser: "app", }, setData: async () => { const dataString = JSON.parse((await AsyncStorage.getItem("data")) || "{}"); - colorScheme.set(dataString.theme || "light"); + colorScheme.set(dataString.theme || "system"); if (dataString) set((state) => ({ data: { ...state.data, ...dataString } }));