Skip to content

Commit

Permalink
feat(mobile): Add ability to manage lists
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Jul 29, 2024
1 parent 92c92c1 commit 93afb75
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 163 deletions.
50 changes: 28 additions & 22 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import "expo-dev-client";

import { useEffect } from "react";
import { View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
import { Stack } from "expo-router/stack";
import { ShareIntentProvider, useShareIntent } from "expo-share-intent";
import { StatusBar } from "expo-status-bar";
import { StyledStack } from "@/components/navigation/stack";
import { Providers } from "@/lib/providers";
import { cn } from "@/lib/utils";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { useColorScheme } from "nativewind";

export default function RootLayout() {
Expand All @@ -28,28 +30,32 @@ export default function RootLayout() {
return (
<ShareIntentProvider>
<Providers>
<View
className={cn(
"w-full flex-1 bg-gray-100 text-foreground dark:bg-background",
colorScheme == "dark" ? "dark" : "light",
)}
>
<StyledStack
contentClassName="bg-gray-100 dark:bg-background"
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="index" />
<Stack.Screen
name="sharing"
options={{
presentation: "modal",
}}
/>
</StyledStack>
<StatusBar style="auto" />
</View>
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<View
className={cn(
"w-full flex-1 bg-gray-100 text-foreground dark:bg-background",
colorScheme == "dark" ? "dark" : "light",
)}
>
<StyledStack
contentClassName="bg-gray-100 dark:bg-background"
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="index" />
<Stack.Screen
name="sharing"
options={{
presentation: "modal",
}}
/>
</StyledStack>
<StatusBar style="auto" />
</View>
</BottomSheetModalProvider>
</GestureHandlerRootView>
</Providers>
</ShareIntentProvider>
);
Expand Down
40 changes: 32 additions & 8 deletions apps/mobile/app/sharing.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { useRouter } from "expo-router";
import { useShareIntentContext } from "expo-share-intent";
import ListPickerModal from "@/components/bookmarks/ListPickerModal";
import { Button } from "@/components/ui/Button";
import useAppSettings from "@/lib/settings";
import { api } from "@/lib/trpc";
import { useUploadAsset } from "@/lib/upload";
import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { z } from "zod";

import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
Expand Down Expand Up @@ -80,18 +83,39 @@ export default function Sharing() {
const router = useRouter();
const [mode, setMode] = useState<Mode>({ type: "idle" });

let autoCloseTimeoutId: NodeJS.Timeout | null = null;
const addToListSheetRef = useRef<BottomSheetModal>(null);

let comp;
switch (mode.type) {
case "idle": {
comp = <SaveBookmark setMode={setMode} />;
break;
}
case "alreadyExists":
case "success": {
comp = <Text className="text-4xl text-foreground">Hoarded!</Text>;
break;
}
case "alreadyExists": {
comp = <Text className="text-4xl text-foreground">Already Hoarded!</Text>;
comp = (
<View className="items-center gap-4">
<ListPickerModal
ref={addToListSheetRef}
snapPoints={["90%"]}
bookmarkId={mode.bookmarkId}
onDismiss={() => router.replace("dashboard")}
/>
<Text className="text-4xl text-foreground">
{mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"}
</Text>
<Button
label="Add to List"
onPress={() => {
addToListSheetRef.current?.present();
if (autoCloseTimeoutId) {
clearTimeout(autoCloseTimeoutId);
}
}}
/>
</View>
);
break;
}
case "error": {
Expand All @@ -106,11 +130,11 @@ export default function Sharing() {
return;
}

const timeoutId = setTimeout(() => {
autoCloseTimeoutId = setTimeout(() => {
router.replace("dashboard");
}, 2000);

return () => clearTimeout(timeoutId);
return () => clearTimeout(autoCloseTimeoutId!);
}, [mode.type]);

return (
Expand Down
20 changes: 20 additions & 0 deletions apps/mobile/components/bookmarks/BookmarkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRef } from "react";
import {
ActivityIndicator,
Image,
Expand All @@ -13,6 +14,7 @@ import { Link } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import useAppSettings from "@/lib/settings";
import { api } from "@/lib/trpc";
import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { MenuView } from "@react-native-menu/menu";
import { Ellipsis, Star } from "lucide-react-native";

Expand All @@ -32,6 +34,7 @@ import { TailwindResolver } from "../TailwindResolver";
import { Divider } from "../ui/Divider";
import { Skeleton } from "../ui/Skeleton";
import { useToast } from "../ui/Toast";
import ListPickerModal from "./ListPickerModal";

function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
const { toast } = useToast();
Expand Down Expand Up @@ -70,6 +73,8 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
onError,
});

const manageListsSheetRef = useRef<BottomSheetModal>(null);

return (
<View className="flex flex-row gap-4">
{(isArchivePending || isDeletionPending) && <ActivityIndicator />}
Expand All @@ -89,6 +94,12 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
)}
</Pressable>

<ListPickerModal
ref={manageListsSheetRef}
snapPoints={["50%", "90%"]}
bookmarkId={bookmark.id}
/>

<MenuView
onPressAction={({ nativeEvent }) => {
Haptics.selectionAsync();
Expand All @@ -101,6 +112,8 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
bookmarkId: bookmark.id,
archived: !bookmark.archived,
});
} else if (nativeEvent.event === "manage_list") {
manageListsSheetRef?.current?.present();
}
}}
actions={[
Expand All @@ -121,6 +134,13 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
ios: "trash",
}),
},
{
id: "manage_list",
title: "Manage Lists",
image: Platform.select({
ios: "list",
}),
},
]}
shouldOpenOnLongPress={false}
>
Expand Down
117 changes: 117 additions & 0 deletions apps/mobile/components/bookmarks/ListPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from "react";
import { Pressable, Text, View } from "react-native";
import Checkbox from "expo-checkbox";
import {
BottomSheetFlatList,
BottomSheetModal,
BottomSheetModalProps,
} from "@gorhom/bottom-sheet";

import {
useAddBookmarkToList,
useBookmarkLists,
useRemoveBookmarkFromList,
} from "@hoarder/shared-react/hooks/lists";
import { api } from "@hoarder/shared-react/trpc";

import PageTitle from "../ui/PageTitle";
import { useToast } from "../ui/Toast";

const ListPickerModal = React.forwardRef<
BottomSheetModal,
Omit<BottomSheetModalProps, "children"> & {
bookmarkId: string;
}
>(({ bookmarkId, ...props }, ref) => {
const { toast } = useToast();
const onError = () => {
toast({
message: "Something went wrong",
variant: "destructive",
showProgress: false,
});
};
const { data: existingLists } = api.lists.getListsOfBookmark.useQuery(
{
bookmarkId,
},
{
select: (data) => new Set(data.lists.map((l) => l.id)),
},
);
const { data } = useBookmarkLists();

const { mutate: addToList } = useAddBookmarkToList({
onSuccess: () => {
toast({
message: `The bookmark has been added to the list!`,
showProgress: false,
});
},
onError,
});

const { mutate: removeToList } = useRemoveBookmarkFromList({
onSuccess: () => {
toast({
message: `The bookmark has been removed from the list!`,
showProgress: false,
});
},
onError,
});

const toggleList = (listId: string) => {
if (!existingLists) {
return;
}
if (existingLists.has(listId)) {
removeToList({ bookmarkId, listId });
} else {
addToList({ bookmarkId, listId });
}
};

const { allPaths } = data ?? {};
return (
<View>
<BottomSheetModal ref={ref} {...props}>
<BottomSheetFlatList
ListHeaderComponent={<PageTitle title="Manage Lists" />}
className="h-full"
contentContainerStyle={{
gap: 5,
}}
renderItem={(l) => (
<View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent">
<Pressable
key={l.item[l.item.length - 1].id}
onPress={() => toggleList(l.item[l.item.length - 1].id)}
className="flex w-full flex-row justify-between"
>
<Text className="text-lg text-accent-foreground">
{l.item
.map((item) => `${item.icon} ${item.name}`)
.join(" / ")}
</Text>
<Checkbox
value={
existingLists &&
existingLists.has(l.item[l.item.length - 1].id)
}
onValueChange={() => {
toggleList(l.item[l.item.length - 1].id);
}}
/>
</Pressable>
</View>
)}
data={allPaths}
/>
</BottomSheetModal>
</View>
);
});
ListPickerModal.displayName = "ListPickerModal";

export default ListPickerModal;
5 changes: 4 additions & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hoarder/trpc": "workspace:^0.1.0",
"@gorhom/bottom-sheet": "^4.6.3",
"@hoarder/shared": "workspace:^0.1.0",
"@hoarder/shared-react": "workspace:^0.1.0",
"@hoarder/trpc": "workspace:^0.1.0",
"@react-native-menu/menu": "^0.9.1",
"@tanstack/react-query": "^5.24.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"expo": "~50.0.11",
"expo-build-properties": "^0.11.1",
"expo-checkbox": "^3.0.0",
"expo-config-plugin-ios-share-extension": "^0.0.4",
"expo-constants": "~15.4.5",
"expo-dev-client": "^3.3.9",
Expand All @@ -40,6 +42,7 @@
"nativewind": "^4.0.1",
"react": "^18.2.0",
"react-native": "0.73.4",
"react-native-gesture-handler": "~2.14.0",
"react-native-markdown-display": "^7.0.2",
"react-native-reanimated": "^3.8.0",
"react-native-safe-area-context": "4.8.2",
Expand Down
Loading

0 comments on commit 93afb75

Please sign in to comment.