Skip to content
Open
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
2 changes: 1 addition & 1 deletion actions/publishToPublication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { AtUri } from "@atproto/syntax";
import { Json } from "supabase/database.types";
import { $Typed, UnicodeString } from "@atproto/api";
import { List, parseBlocksToList } from "src/utils/parseBlocksToList";
import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks";
import { getBlocksWithTypeLocal } from "src/replicache/getBlocks";
import { Lock } from "src/utils/lock";
import type { PubLeafletPublication } from "lexicons/api";
import {
Expand Down
2 changes: 1 addition & 1 deletion actions/subscriptions/subscribeToMailboxWithEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { and, eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import { email_subscriptions_to_entity } from "drizzle/schema";
import postgres from "postgres";
import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks";
import { getBlocksWithTypeLocal } from "src/replicache/getBlocks";
import type { Fact, PermissionToken } from "src/replicache";
import type { Attribute } from "src/replicache/attributes";
import { Database } from "supabase/database.types";
Expand Down
115 changes: 106 additions & 9 deletions components/Blocks/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { HorizontalRule } from "./HorizontalRule";
import { deepEquals } from "src/utils/deepEquals";
import { isTextBlock } from "src/utils/isTextBlock";
import { focusPage } from "src/utils/focusPage";
import { getBlocksWithType } from "src/replicache/getBlocks";

export type Block = {
factID: string;
Expand All @@ -42,6 +43,8 @@ export type Block = {
type: Fact<"block/type">["data"]["value"];
listData?: {
checklist?: boolean;
listStyle?: "ordered" | "unordered";
listNumber?: number;
path: { depth: number; entity: string }[];
parent: string;
depth: number;
Expand Down Expand Up @@ -172,7 +175,9 @@ function deepEqualsBlockProps(
if (
prevProps.listData.checklist !== nextProps.listData.checklist ||
prevProps.listData.parent !== nextProps.listData.parent ||
prevProps.listData.depth !== nextProps.listData.depth
prevProps.listData.depth !== nextProps.listData.depth ||
prevProps.listData.listNumber !== nextProps.listData.listNumber ||
prevProps.listData.listStyle !== nextProps.listData.listStyle
) {
return false;
}
Expand Down Expand Up @@ -420,6 +425,7 @@ export const ListMarker = (
) => {
let isMobile = useIsMobile();
let checklist = useEntity(props.value, "block/check-list");
let listStyle = useEntity(props.value, "block/list-style");
let headingLevel = useEntity(props.value, "block/heading-level")?.data.value;
let children = useEntity(props.value, "card/block");
let folded =
Expand All @@ -429,6 +435,62 @@ export const ListMarker = (
let depth = props.listData?.depth;
let { permissions } = useEntitySetContext();
let { rep } = useReplicache();

let [editingNumber, setEditingNumber] = useState(false);
let [numberInputValue, setNumberInputValue] = useState("");

useEffect(() => {
if (!editingNumber) {
setNumberInputValue("");
}
}, [editingNumber]);

const handleNumberSave = async () => {
if (!rep || !props.listData) return;

const newNumber = parseInt(numberInputValue, 10);
if (isNaN(newNumber) || newNumber < 1) {
setEditingNumber(false);
return;
}

const oldNumber = props.listData.listNumber || 1;
const difference = newNumber - oldNumber;

if (difference === 0) {
setEditingNumber(false);
return;
}

// Update this block's number
await rep.mutate.assertFact({
entity: props.value,
attribute: "block/list-number",
data: { type: "number", value: newNumber },
});

// Cascade to following blocks at the same depth
const allBlocks = await rep.query((tx) => getBlocksWithType(tx, props.parent));
if (allBlocks) {
const currentIndex = allBlocks.findIndex((b) => b.value === props.value);
for (let i = currentIndex + 1; i < allBlocks.length; i++) {
const block = allBlocks[i];
if (
block.listData?.listStyle === "ordered" &&
block.listData?.depth === props.listData.depth
) {
const currentNumber = block.listData.listNumber || 1;
await rep.mutate.assertFact({
entity: block.value,
attribute: "block/list-number",
data: { type: "number", value: currentNumber + difference },
});
}
}
}

setEditingNumber(false);
};
return (
<div
className={`shrink-0 flex justify-end items-center h-3 z-1
Expand Down Expand Up @@ -456,14 +518,49 @@ export const ListMarker = (
}}
className={`listMarker group/list-marker p-2 ${children.length > 0 ? "cursor-pointer" : "cursor-default"}`}
>
<div
className={`h-[5px] w-[5px] rounded-full bg-secondary shrink-0 right-0 outline outline-offset-1
${
folded
? "outline-secondary"
: ` ${children.length > 0 ? "sm:group-hover/list-marker:outline-secondary outline-transparent" : "outline-transparent"}`
}`}
/>
{listStyle?.data.value === "ordered" ? (
editingNumber ? (
<input
type="text"
value={numberInputValue}
onChange={(e) => setNumberInputValue(e.target.value)}
onClick={(e) => e.stopPropagation()}
onBlur={handleNumberSave}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleNumberSave();
} else if (e.key === "Escape") {
setEditingNumber(false);
}
}}
autoFocus
className="text-secondary font-normal text-right min-w-[2rem] w-[2.2rem] bg-transparent border border-accent focus:outline-none px-1"
/>
) : (
<div
className="text-secondary font-normal text-right min-w-[2rem] cursor-pointer hover:text-primary"
onClick={(e) => {
e.stopPropagation();
if (permissions.write && listStyle?.data.value === "ordered") {
setNumberInputValue(String(props.listData?.listNumber || 1));
setEditingNumber(true);
}
}}
>
{props.listData?.listNumber || 1}.
</div>
)
) : (
<div
className={`h-[5px] w-[5px] rounded-full bg-secondary shrink-0 right-0 outline outline-offset-1
${
folded
? "outline-secondary"
: ` ${children.length > 0 ? "sm:group-hover/list-marker:outline-secondary outline-transparent" : "outline-transparent"}`
}`}
/>
)}
</button>
{checklist && (
<button
Expand Down
30 changes: 28 additions & 2 deletions components/Blocks/BlockCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from "components/Icons/BlockTextSmall";
import { LinkSmall } from "components/Icons/LinkSmall";
import { BlockRSVPSmall } from "components/Icons/BlockRSVPSmall";
import { ListUnorderedSmall } from "components/Toolbar/ListToolbar";
import { ListUnorderedSmall, ListOrderedSmall } from "components/Toolbar/ListToolbar";
import { BlockMathSmall } from "components/Icons/BlockMathSmall";
import { BlockCodeSmall } from "components/Icons/BlockCodeSmall";
import { QuoteSmall } from "components/Icons/QuoteSmall";
Expand Down Expand Up @@ -151,7 +151,7 @@ export const blockCommands: Command[] = [
},
},
{
name: "List",
name: "Unordered List",
icon: <ListUnorderedSmall />,
type: "text",
onSelect: async (rep, props, um) => {
Expand All @@ -164,6 +164,32 @@ export const blockCommands: Command[] = [
clearCommandSearchText(entity);
},
},
{
name: "Ordered List",
icon: <ListOrderedSmall />,
type: "text",
onSelect: async (rep, props, um) => {
let entity = await createBlockWithType(rep, props, "text");
await rep?.mutate.assertFact([
{
entity,
attribute: "block/is-list",
data: { value: true, type: "boolean" },
},
{
entity,
attribute: "block/list-style",
data: { value: "ordered", type: "list-style-union" },
},
{
entity,
attribute: "block/list-number",
data: { value: 1, type: "number" },
},
]);
clearCommandSearchText(entity);
},
},
{
name: "Block Quote",
icon: <QuoteSmall />,
Expand Down
2 changes: 1 addition & 1 deletion components/Blocks/MailboxBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { confirmEmailSubscription } from "actions/subscriptions/confirmEmailSubs
import { focusPage } from "src/utils/focusPage";
import { v7 } from "uuid";
import { sendPostToSubscribers } from "actions/subscriptions/sendPostToSubscribers";
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
import { getBlocksWithType } from "src/replicache/getBlocks";
import { getBlocksAsHTML } from "src/utils/getBlocksAsHTML";
import { htmlToMarkdown } from "src/htmlMarkdownParsers";
import {
Expand Down
43 changes: 38 additions & 5 deletions components/Blocks/TextBlock/inputRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,44 @@ export const inputrules = (
if (propsRef.current.listData) return null;
let tr = state.tr;
tr.delete(0, 2);
repRef.current?.mutate.assertFact({
entity: propsRef.current.entityID,
attribute: "block/is-list",
data: { type: "boolean", value: true },
});
repRef.current?.mutate.assertFact([
{
entity: propsRef.current.entityID,
attribute: "block/is-list",
data: { type: "boolean", value: true },
},
{
entity: propsRef.current.entityID,
attribute: "block/list-style",
data: { type: "list-style-union", value: "unordered" },
},
]);
return tr;
}),

// Ordered List - respect the starting number typed
new InputRule(/^(\d+)\.\s$/, (state, match) => {
if (propsRef.current.listData) return null;
let tr = state.tr;
tr.delete(0, match[0].length);
const startNumber = parseInt(match[1], 10);
repRef.current?.mutate.assertFact([
{
entity: propsRef.current.entityID,
attribute: "block/is-list",
data: { type: "boolean", value: true },
},
{
entity: propsRef.current.entityID,
attribute: "block/list-style",
data: { type: "list-style-union", value: "ordered" },
},
{
entity: propsRef.current.entityID,
attribute: "block/list-number",
data: { type: "number", value: startNumber },
},
]);
return tr;
}),

Expand Down
67 changes: 63 additions & 4 deletions components/Blocks/TextBlock/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { focusPage } from "src/utils/focusPage";
import { v7 } from "uuid";
import { scanIndex } from "src/replicache/utils";
import { indent, outdent } from "src/utils/list-operations";
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
import { getBlocksWithType } from "src/replicache/getBlocks";
import { isTextBlock } from "src/utils/isTextBlock";
import { UndoManager } from "src/undoManager";
type PropsRef = RefObject<
Expand Down Expand Up @@ -369,11 +369,15 @@ const shifttab =
propsRef: RefObject<BlockProps & { entity_set: { set: string } }>,
repRef: RefObject<Replicache<ReplicacheMutators> | null>,
) =>
() => {
async () => {
if (useUIState.getState().selectedBlocks.length > 1) return false;
if (!repRef.current) return false;
if (!repRef.current) return false;
outdent(propsRef.current, propsRef.current.previousBlock, repRef.current);
let { foldedBlocks, toggleFold } = useUIState.getState();
await outdent(propsRef.current, propsRef.current.previousBlock, repRef.current, {
foldedBlocks,
toggleFold,
});
return true;
};

Expand Down Expand Up @@ -423,12 +427,40 @@ const enter =
y: position.data.position.y + box.height,
},
});
if (propsRef.current.listData)
if (propsRef.current.listData) {
await repRef.current?.mutate.assertFact({
entity: newEntityID,
attribute: "block/is-list",
data: { type: "boolean", value: true },
});
// Copy list style for canvas blocks
let listStyle = await repRef.current?.query((tx) =>
scanIndex(tx).eav(propsRef.current.entityID, "block/list-style"),
);
if (listStyle?.[0])
await repRef.current?.mutate.assertFact({
entity: newEntityID,
attribute: "block/list-style",
data: {
type: "list-style-union",
value: listStyle[0].data.value,
},
});
// Set list number to next number if this is an ordered list
if (listStyle?.[0]) {
let listNumber = await repRef.current?.query((tx) =>
scanIndex(tx).eav(propsRef.current.entityID, "block/list-number"),
);
await repRef.current?.mutate.assertFact({
entity: newEntityID,
attribute: "block/list-number",
data: {
type: "number",
value: (listNumber?.[0]?.data.value || 0) + 1,
},
});
}
}
return;
}
if (propsRef.current.listData) {
Expand Down Expand Up @@ -499,6 +531,33 @@ const enter =
attribute: "block/is-list",
data: { type: "boolean", value: true },
});
// Copy list style (ordered/unordered) to new list item
let listStyle = await repRef.current?.query((tx) =>
scanIndex(tx).eav(propsRef.current.entityID, "block/list-style"),
);
if (listStyle?.[0])
await repRef.current?.mutate.assertFact({
entity: newEntityID,
attribute: "block/list-style",
data: {
type: "list-style-union",
value: listStyle[0].data.value,
},
});
// Set list number to next number if this is an ordered list
if (listStyle?.[0]) {
let listNumber = await repRef.current?.query((tx) =>
scanIndex(tx).eav(propsRef.current.entityID, "block/list-number"),
);
await repRef.current?.mutate.assertFact({
entity: newEntityID,
attribute: "block/list-number",
data: {
type: "number",
value: (listNumber?.[0]?.data.value || 0) + 1,
},
});
}
let checked = await repRef.current?.query((tx) =>
scanIndex(tx).eav(propsRef.current.entityID, "block/check-list"),
);
Expand Down
Loading