Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions apps/ios/app/archive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import { Icon, ICONS } from "@/components/Glyph";
import { ItemRow } from "@/components/ItemRow";
import { Empty, usePull } from "@/components/Screen";
import { toast } from "@/components/Toast";
import { keys, useArchive, useMe, usePatchItem } from "@/lib/data";
import { keys, library, useArchive, useMe, usePatchItem } from "@/lib/data";
import { useRowText } from "@/lib/format";
import { kv } from "@/lib/kv";
import { api } from "@/lib/session";
import { font, scale, useTheme } from "@/lib/theme";

const FOLD = "fold:archive"; // the months folded away, remembered on this phone
Expand Down Expand Up @@ -78,13 +77,11 @@ export default function Archive() {
qc.setQueryData(keys.archive, without);
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
try {
await api(`/api/v1/items/${item.id}`, { method: "DELETE" });
await library.remove(item.id);
} catch (e) {
void qc.invalidateQueries({ queryKey: keys.archive });
return toast(e instanceof Refused && e.status === 403 ? tr("archive.needsSignIn") : tr("common.failed.title"));
}
void qc.invalidateQueries({ queryKey: keys.bin });
toast(tr("moved.deleted"), () => void api(`/api/v1/items/${item.id}/restore`, { method: "POST" }).then(() => qc.invalidateQueries({ queryKey: ["items"] })));
toast(tr("moved.deleted"), () => void library.fromBin(item.id));
};

return (
Expand Down
9 changes: 3 additions & 6 deletions apps/ios/app/bin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import { Icon, ICONS } from "@/components/Glyph";
import { ItemRow } from "@/components/ItemRow";
import { Empty, usePull } from "@/components/Screen";
import { toast } from "@/components/Toast";
import { keys, useBin } from "@/lib/data";
import { keys, library, useBin } from "@/lib/data";
import { useRowText } from "@/lib/format";
import { api } from "@/lib/session";
import { font, scale, useTheme } from "@/lib/theme";

const DAY = 86_400_000;
Expand All @@ -35,19 +34,17 @@ export default function Bin() {

const restore = async (item: Item) => {
drop(item);
await api(`/api/v1/items/${item.id}/restore`, { method: "POST" }).catch(() => toast(tr("common.failed.title")));
void qc.invalidateQueries({ queryKey: ["items"] });
await library.fromBin(item.id).catch(() => toast(tr("common.failed.title")));
toast(tr("archive.outOfBin"));
};

const forever = async (item: Item) => {
drop(item);
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
try {
await api(`/api/v1/items/${item.id}/forever`, { method: "DELETE" });
await library.forever(item.id);
toast(tr("archive.gone"));
} catch (e) {
void qc.invalidateQueries({ queryKey: keys.bin });
toast(e instanceof Refused && e.status === 403 ? tr("archive.needsSignIn") : tr("common.failed.title"));
}
};
Expand Down
26 changes: 5 additions & 21 deletions apps/ios/app/lists/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { ItemRow } from "@/components/ItemRow";
import { Empty } from "@/components/Screen";
import { toast } from "@/components/Toast";
import { ToolBar } from "@/components/ToolBar";
import { keys, useList } from "@/lib/data";
import { keys, library, useList } from "@/lib/data";
import { kv } from "@/lib/kv";
import { api } from "@/lib/session";
import { font, scale, useTheme } from "@/lib/theme";

type Row = { e: ListEntry; n: number } | "head";
Expand Down Expand Up @@ -52,42 +51,27 @@ export default function ListScreen() {
const chosen = entries.filter((e) => picked?.has(e.item.id));

const edit = (fn: (entries: ListEntry[]) => ListEntry[]) => qc.setQueryData<Data>(keys.list(id), (old) => old && { ...old, entries: fn(old.entries) });
const refresh = () => {
void qc.invalidateQueries({ queryKey: keys.list(id) });
void qc.invalidateQueries({ queryKey: keys.lists });
void qc.invalidateQueries({ queryKey: ["items"] }); // a Todo marked here is Done there
void qc.invalidateQueries({ queryKey: ["counts"] });
};
const failed = () => toast(tr("common.failed.title"));

const mark = async (ids: string[], ticked: boolean) => {
edit((es) => es.map((e) => (ids.includes(e.item.id) ? { ...e, ticked } : e)));
void Haptics.selectionAsync();
await (ids.length === 1
? api(`/api/v1/lists/${id}/items/${ids[0]}`, { method: "PATCH", json: { ticked } })
: api(`/api/v1/lists/${id}/items`, { method: "PATCH", json: { itemIds: ids, ticked } })
).catch(failed);
refresh();
await library.mark(id, ids, ticked).catch(failed);
};

const toTop = async () => {
const order = [...chosen, ...entries.filter((e) => !picked?.has(e.item.id))];
setPicked(null);
edit(() => order);
await api(`/api/v1/lists/${id}/reorder`, { json: { itemIds: order.map((e) => e.item.id) } }).catch(failed);
refresh();
await library.reorder(id, order.map((e) => e.item.id)).catch(failed);
};

const takeOut = async () => {
const gone = chosen;
setPicked(null);
edit((es) => es.filter((e) => !gone.includes(e)));
for (const e of gone) await api(`/api/v1/lists/${id}/items/${e.item.id}`, { method: "DELETE" }).catch(failed);
refresh();
toast(tr("list.takenOut", { count: gone.length }), async () => {
for (const e of gone) await api(`/api/v1/lists/${id}/items`, { json: { itemId: e.item.id, position: e.position, note: e.note } }).catch(failed);
refresh();
});
await library.setInList(gone.map((e) => e.item.id), id, false).catch(failed);
toast(tr("list.takenOut", { count: gone.length }), () => void library.putBack(id, gone).catch(failed));
};

const share = () => router.push({ pathname: "/share", params: { id } });
Expand Down
43 changes: 12 additions & 31 deletions apps/ios/app/pick.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Item, ItemPage, List, Tag } from "@pluck/shared-schemas";
import type { Item, ItemPage } from "@pluck/shared-schemas";
import { useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router";
import { useState, type ReactNode } from "react";
Expand All @@ -7,8 +7,7 @@ import { useTranslations } from "use-intl";
import { Icon, ICONS } from "@/components/Glyph";
import { ItemRow } from "@/components/ItemRow";
import { Empty } from "@/components/Screen";
import { keys, useItem, useItems, useList, useLists, usePatchItem, useTags } from "@/lib/data";
import { api } from "@/lib/session";
import { library, useItem, useItems, useList, useLists, usePatchItem, useTags } from "@/lib/data";
import { font, scale, useTheme } from "@/lib/theme";

// One sheet for three picks (no board: built from the web pickers): an item's tags, an item's lists,
Expand Down Expand Up @@ -69,7 +68,6 @@ function Row({ label, on, onPress, left }: { label: string; on: boolean; onPress
// Search your tags, or make one from what you typed: one tap either way.
function TagsFor({ itemId }: { itemId: string }) {
const tr = useTranslations("ios.pick");
const qc = useQueryClient();
const item = useItem(itemId);
const tags = useTags();
const patch = usePatchItem();
Expand All @@ -79,10 +77,9 @@ function TagsFor({ itemId }: { itemId: string }) {
const shown = (tags.data ?? []).filter((x) => x.name.toLowerCase().includes(name.toLowerCase()));
const fresh = name && !(tags.data ?? []).some((x) => x.name.toLowerCase() === name.toLowerCase());
const make = async () => {
const { tag } = await api<{ tag: Tag }>("/api/v1/tags", { json: { name } });
const tag = await library.newTag(name);
patch.mutate({ id: itemId, patch: { tagIds: [...on, tag.id] } });
setFilter("");
void qc.invalidateQueries({ queryKey: keys.tags });
};
return (
<Sheet
Expand All @@ -101,28 +98,21 @@ function TagsFor({ itemId }: { itemId: string }) {
// The lists an item is in, a tap to add or take it out, and a new list from what you typed.
function ListsFor({ itemId }: { itemId: string }) {
const tr = useTranslations("ios.pick");
const qc = useQueryClient();
const item = useItem(itemId);
const lists = useLists();
const [filter, setFilter] = useState("");
const inLists = new Set((item.data?.lists ?? []).map((m) => m.list.id));
const name = filter.trim();
const shown = (lists.data ?? []).filter((l) => l.name.toLowerCase().includes(name.toLowerCase()));
const fresh = name && !(lists.data ?? []).some((l) => l.name.toLowerCase() === name.toLowerCase());
const settle = async (listId?: string) => {
await item.refetch();
void qc.invalidateQueries({ queryKey: keys.lists });
if (listId) void qc.invalidateQueries({ queryKey: keys.list(listId) });
};
const toggle = async (listId: string) => {
if (inLists.has(listId)) await api(`/api/v1/lists/${listId}/items/${itemId}`, { method: "DELETE" }).catch(() => {});
else await api(`/api/v1/lists/${listId}/items`, { json: { itemId } }).catch(() => {});
await settle(listId);
await library.setInList([itemId], listId, !inLists.has(listId)).catch(() => {});
await item.refetch();
};
const start = async () => {
await api<{ list: List }>("/api/v1/lists", { json: { name, items: [{ itemId }] } });
await library.newList(name, [itemId]);
setFilter("");
await settle();
await item.refetch();
};
return (
<Sheet
Expand Down Expand Up @@ -153,12 +143,10 @@ function TagsForMany({ ids }: { ids: string[] }) {
const fresh = name && !(tags.data ?? []).some((x) => x.name.toLowerCase() === name.toLowerCase());
const apply = async (tagId: string, on: boolean) => {
setAll((cur) => { const next = new Set(cur); if (on) next.add(tagId); else next.delete(tagId); return next; });
await api("/api/v1/items/batch", { json: { itemIds: ids, patch: on ? { addTagIds: [tagId] } : { removeTagIds: [tagId] } } }).catch(() => {});
void qc.invalidateQueries({ queryKey: ["items"] });
void qc.invalidateQueries({ queryKey: keys.tags });
await library.setTag(ids, tagId, on, loaded).catch(() => {});
};
const make = async () => {
const { tag } = await api<{ tag: Tag }>("/api/v1/tags", { json: { name } });
const tag = await library.newTag(name);
setFilter("");
await apply(tag.id, true);
};
Expand All @@ -177,7 +165,6 @@ function TagsForMany({ ids }: { ids: string[] }) {
// Lists for many selected rows: a tap adds them all; a new list can start with them.
function ListsForMany({ ids }: { ids: string[] }) {
const tr = useTranslations("ios.pick");
const qc = useQueryClient();
const lists = useLists();
const [filter, setFilter] = useState("");
const [added, setAdded] = useState<Set<string>>(new Set());
Expand All @@ -187,15 +174,12 @@ function ListsForMany({ ids }: { ids: string[] }) {
const add = async (listId: string) => {
if (added.has(listId)) return;
setAdded((cur) => new Set(cur).add(listId));
await api(`/api/v1/lists/${listId}/items`, { json: { itemIds: ids } }).catch(() => {});
void qc.invalidateQueries({ queryKey: keys.lists });
void qc.invalidateQueries({ queryKey: keys.list(listId) });
await library.setInList(ids, listId, true).catch(() => {});
};
const start = async () => {
const { list } = await api<{ list: List }>("/api/v1/lists", { json: { name, items: ids.map((itemId) => ({ itemId })) } });
const list = await library.newList(name, ids);
setFilter("");
setAdded((cur) => new Set(cur).add(list.id));
void qc.invalidateQueries({ queryKey: keys.lists });
};
return (
<Sheet
Expand Down Expand Up @@ -231,17 +215,14 @@ function Finder({ value, onChange, label, make }: { value: string; onChange: (v:
function AddTo({ listId }: { listId: string }) {
const t = useTheme();
const tr = useTranslations("ios.pick");
const qc = useQueryClient();
const list = useList(listId);
const inbox = useItems("inbox");
const later = useItems("later");
const inList = new Set((list.data?.entries ?? []).map((e) => e.item.id));
const candidates: Item[] = [...(inbox.data?.items ?? []), ...(later.data?.items ?? [])];
const add = async (itemId: string) => {
if (inList.has(itemId)) return;
await api(`/api/v1/lists/${listId}/items`, { json: { itemId } }).catch(() => {});
await list.refetch();
void qc.invalidateQueries({ queryKey: keys.lists });
await library.setInList([itemId], listId, true).catch(() => {});
};
return (
<Sheet
Expand Down
13 changes: 3 additions & 10 deletions apps/ios/app/tags.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
import type { Tag } from "@pluck/shared-schemas";
import { Refused } from "@pluck/client";
import { useQueryClient } from "@tanstack/react-query";
import { router } from "expo-router";
import { ActionSheetIOS, Alert, FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { useTranslations } from "use-intl";
import { Empty } from "@/components/Screen";
import { toast } from "@/components/Toast";
import { keys, useTags } from "@/lib/data";
import { api } from "@/lib/session";
import { library, useTags } from "@/lib/data";
import { font, scale, useTheme } from "@/lib/theme";

// Your tags, from Settings (the web's Tags section): rename one, or take it off everything. Items never go with it.
export default function Tags() {
const t = useTheme();
const tr = useTranslations("ios");
const qc = useQueryClient();
const tags = useTags();
const settle = () => {
void qc.invalidateQueries({ queryKey: keys.tags });
void qc.invalidateQueries({ queryKey: ["items"] });
};
const failed = (e: unknown) => Alert.alert(tr("common.failed.title"), e instanceof Refused ? e.message : tr("common.failed.body"));
const rename = (tag: Tag) =>
Alert.prompt(tr("tags.renameTitle"), undefined, [
{ text: tr("common.cancel"), style: "cancel" },
{ text: tr("common.save"), onPress: (name?: string) => void api(`/api/v1/tags/${tag.id}`, { method: "PATCH", json: { name: name ?? "" } }).then(settle).catch(failed) },
{ text: tr("common.save"), onPress: (name?: string) => void library.renameTag(tag.id, name ?? "").catch(failed) },
], "plain-text", tag.name);
const remove = (tag: Tag) =>
Alert.alert(tr("tags.deleteTitle", { name: tag.name }), tr("tags.deleteBody", { count: tag.count }), [
{ text: tr("common.cancel"), style: "cancel" },
{
text: tr("tags.deleteTag"), style: "destructive", onPress: () =>
void api(`/api/v1/tags/${tag.id}?removeFromItems=true`, { method: "DELETE" }).then(() => { settle(); toast(tr("tags.deleted", { name: tag.name })); }).catch(failed),
void library.deleteTag(tag.id).then(() => toast(tr("tags.deleted", { name: tag.name }))).catch(failed),
},
]);
const open = (tag: Tag) =>
Expand Down
11 changes: 3 additions & 8 deletions apps/ios/components/ItemsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { ActionSheetIOS, ActivityIndicator, Pressable, RefreshControl, StyleSheet, Text, View } from "react-native";
import Animated, { FadeInUp } from "react-native-reanimated";
import { useTranslations } from "use-intl";
import { api } from "@/lib/session";
import { keys, shareExtensionCache, syncPending, useCounts, useItems, useLists, useMe, useOnline, usePatchItem, usePending, useTags } from "@/lib/data";
import { keys, library, shareExtensionCache, syncPending, useCounts, useItems, useLists, useMe, useOnline, usePatchItem, usePending, useTags } from "@/lib/data";
import { font, scale, useTheme } from "@/lib/theme";
import { ShareRow, SwipeToLater } from "./Art";
import { Icon, ICONS } from "./Glyph";
Expand Down Expand Up @@ -80,12 +79,8 @@ export function ItemsScreen({ status }: { status: "inbox" | "later" }) {
setPicked(null);
qc.setQueryData(keys.items(status), (old: typeof items.data) => (old ? { ...old, items: old.items.filter((i) => !ids.includes(i.id)) } : old));
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const send = (s: Status) => api("/api/v1/items/batch", { json: { itemIds: ids, patch: { status: s } } }).finally(() => {
void qc.invalidateQueries({ queryKey: ["items"] });
void qc.invalidateQueries({ queryKey: ["counts"] });
});
await send(to).catch(() => toast(tr("items.moveFailed")));
toast(tr(`movedMany.${to}`, { count: ids.length }), () => void send(status));
await library.move(ids, to).catch(() => toast(tr("items.moveFailed")));
toast(tr(`movedMany.${to}`, { count: ids.length }), () => void library.restore(moved));
};

const away: Status = status === "later" ? "inbox" : "later";
Expand Down
Loading
Loading