Skip to content

Commit

Permalink
Merge branch 'preview'
Browse files Browse the repository at this point in the history
  • Loading branch information
pdelfan committed Sep 2, 2024
2 parents eaefca5 + 8fb8bdf commit 2b5ef41
Show file tree
Hide file tree
Showing 8 changed files with 627 additions and 55 deletions.
424 changes: 424 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-query": "^5.29.2",
"@tiptap/extension-character-count": "2.1.16",
"@tiptap/extension-link": "2.1.16",
Expand Down
46 changes: 37 additions & 9 deletions src/components/dataDisplay/postText/postText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { RichText as RichTextHelper, AppBskyFeedPost } from "@atproto/api";
import type { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs";
import Link from "next/link";
import { Fragment } from "react";
import * as Tooltip from "@radix-ui/react-tooltip";
import { BiLinkExternal } from "react-icons/bi";

interface Props {
record: PostView["record"];
Expand Down Expand Up @@ -43,15 +45,41 @@ export default function PostText(props: Props) {
content.push({
text: segment.text,
component: (
<Link
className="text-skin-link-base hover:text-skin-link-hover break-all"
href={segment.link?.uri!}
target="blank"
key={segment.link?.uri}
onClick={(e) => e.stopPropagation()}
>
{segment.text}
</Link>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Link
className="text-skin-link-base hover:text-skin-link-hover break-all"
href={segment.link?.uri!}
target="blank"
key={segment.link?.uri}
onClick={(e) => e.stopPropagation()}
>
{segment.text}
</Link>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="bg-skin-base z-[60] p-3 border border-skin-base rounded-xl max-w-xs shadow-lg m-3">
<div className="flex flex-wrap items-center gap-2 text-lg">
<span className="block text-skin-base font-medium">
Link
</span>
<BiLinkExternal />
</div>

<Link
className="text-skin-link-base hover:text-skin-link-hover break-all"
href={segment.link?.uri!}
target="blank"
key={segment.link?.uri}
onClick={(e) => e.stopPropagation()}
>
{segment.link?.uri}
</Link>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
),
});
} else if (segment.isTag()) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/inputs/editor/BottomEditorBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RichText } from "@atproto/api";
import { jsonToText } from "@/lib/utils/text";
import ThreadGatePicker from "./ThreadGatePicker";
import { ThreadgateSetting } from "../../../../types/feed";
import LinkPicker from "./LinkPicker";

interface Props {
editor: Editor;
Expand Down Expand Up @@ -81,7 +82,7 @@ export default function BottomEditorBar(props: Props) {
disabled={!images || images.length === 0}
/>
<ImagePicker onShow={setShowDropzone} />
{/* <LinkPicker editor={editor} /> */}
<LinkPicker editor={editor} />
</div>
<div className="just flex flex-wrap gap-x-5 gap-y-2">
<LanguagePicker
Expand Down
107 changes: 71 additions & 36 deletions src/components/inputs/editor/LinkPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Editor } from "@tiptap/react";
import Button from "@/components/actions/button/Button";
import { useState } from "react";
import Popover from "@/components/actions/popover/Popover";
import * as Dialog from "@radix-ui/react-dialog";
import Input from "../input/Input";
import { BiLink, BiUnlink } from "react-icons/bi";
import { BiPlus } from "react-icons/bi";
import { isValidUrl } from "@/lib/utils/link";

interface Props {
editor: Editor;
Expand All @@ -15,35 +16,50 @@ export default function LinkPicker(props: Props) {
const { selection } = editor.state;
const [showLinkPicker, setShowLinkPicker] = useState(false);
const [href, setHref] = useState("");
const [showError, setShowError] = useState(false);

const onClose = () => {
setShowLinkPicker(false);
setShowError(false);
editor.commands.focus();
};

const onAddLink = (href: string) => {
if (!isValidUrl(href)) {
setShowError(true);
return;
}

const onLinkAdd = (href: string) => {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: href })
.focus()
.run();

onClose();
};

const onLinkRemove = () => {
const onRemoveLink = () => {
if (editor.isActive("link")) {
editor.chain().focus().unsetLink().run();
}
onClose();
};

return (
<Popover>
<Popover.Trigger>
<Dialog.Root open={showLinkPicker} onOpenChange={setShowLinkPicker}>
<Dialog.Trigger>
<Button
disabled={!editor.isActive("link") && selection.empty}
onClick={(e) => {
e.stopPropagation();
if (editor.isActive("link")) {
setShowLinkPicker(false);
onLinkRemove();
onRemoveLink();
} else {
setShowLinkPicker(true);
setShowError(false);
}
}}
className="p-0"
Expand All @@ -54,34 +70,53 @@ export default function LinkPicker(props: Props) {
<BiLink className="text-2xl text-primary hover:text-primary-dark" />
)}
</Button>
</Popover.Trigger>
<Popover.Content>
{showLinkPicker && (
<div className="flex gap-2">
<Input
placeholder="https://your-link.com"
onChange={(e) => setHref(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
onLinkAdd(e.currentTarget.value);
setShowLinkPicker(false);
}
}}
/>
<Button
onClick={() => {
if (editor.isActive("link")) {
setShowLinkPicker(false);
}
onLinkAdd(href);
}}
className="bg-primary text-white p-3 rounded-lg hover:bg-primary-dark"
>
<BiPlus className="text-lg" />
</Button>
</div>
)}
</Popover.Content>
</Popover>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="bg-skin-overlay-muted fixed inset-0 z-50" />
<div className="fixed inset-0 z-50 flex items-center justify-center">
<Dialog.Content>
<div className="flex flex-wrap flex-col gap-2 bg-skin-base p-2 border-skin-base border rounded-xl">
<div className="flex flex-wrap gap-2">
<Input
type="url"
placeholder="https://your-link.com"
onChange={(e) => {
setHref(e.currentTarget.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
onAddLink(e.currentTarget.value);
}
}}
onInput={() => setShowError(false)}
/>
</div>
{showError && (
<small className="text-status-danger block font-medium">
Invalid URL
</small>
)}

<div className="mt-2 flex justify-end items-center gap-2">
<Button
onClick={onClose}
className="hover:bg-skin-secondary border-skin-base text-skin-base rounded-full border px-4 py-2 text-sm font-semibold"
>
Cancel
</Button>
<Button
onClick={() => {
onAddLink(href);
}}
className="bg-primary hover:bg-primary-dark text-white rounded-full px-4 py-2 text-sm font-semibold"
>
Add link
</Button>
</div>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
}
19 changes: 14 additions & 5 deletions src/lib/hooks/bsky/feed/usePublishPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { compressImage } from "@/lib/utils/image";
import { JSONContent } from "@tiptap/react";
import toast from "react-hot-toast";
import { ThreadgateSetting } from "../../../../../types/feed";
import { getLinkFacets } from "@/lib/utils/link";

interface Props {
text: JSONContent;
Expand Down Expand Up @@ -50,11 +51,19 @@ export default function usePublishPost(props: Props) {
mutationKey: ["publishPost"],
mutationFn: async () => {
const richText = new RichText({ text: jsonToText(text) });
const linkFacets = getLinkFacets(text);
await richText.detectFacets(agent);

// add link facets if they exist
if (Array.isArray(richText.facets)) {
richText.facets = [...richText.facets, ...linkFacets];
} else {
richText.facets = [...linkFacets];
}

if (richText.graphemeLength > MAX_POST_LENGTH) {
throw new Error(
"Post length exceeds the maximum length of 300 characters",
"Post length exceeds the maximum length of 300 characters"
);
}

Expand Down Expand Up @@ -129,7 +138,7 @@ export default function usePublishPost(props: Props) {
new Uint8Array(await blob.arrayBuffer()),
{
encoding: blob.type,
},
}
);

embedImages.images.push({
Expand Down Expand Up @@ -191,13 +200,13 @@ export default function usePublishPost(props: Props) {
try {
const image = await fetch(linkCard.image);
const blob = await compressImage(
(await image.blob()) as UploadImage,
(await image.blob()) as UploadImage
);
const uploaded = await agent.uploadBlob(
new Uint8Array(await blob.arrayBuffer()),
{
encoding: blob.type,
},
}
);
embedExternal.external.thumb = uploaded.data.blob;
} catch (e) {
Expand Down Expand Up @@ -246,7 +255,7 @@ export default function usePublishPost(props: Props) {

await agent.api.app.bsky.feed.threadgate.create(
{ repo: agent.session!.did, rkey: submittedPost.rkey },
{ post: result.uri, createdAt: new Date().toISOString(), allow },
{ post: result.uri, createdAt: new Date().toISOString(), allow }
);
}
},
Expand Down
78 changes: 76 additions & 2 deletions src/lib/utils/link.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppBskyRichtextFacet, RichText } from "@atproto/api";
import { AppBskyRichtextFacet, RichText, UnicodeString } from "@atproto/api";
import { JSONContent } from "@tiptap/react";
import { jsonToText } from "./text";

Expand Down Expand Up @@ -26,4 +26,78 @@ export function detectLinksInEditor(json: JSONContent) {
}

return set;
}
}

function getByteLength(string: string) {
const Unicode = new UnicodeString(string);
return Unicode.length;
}

// https://docs.bsky.app/docs/advanced-guides/post-richtext#rich-text-facets
function getFacetFromMark(mark: any, text: string, length: number) {
if (mark.type !== "link") {
return undefined;
}

const link = mark.attrs.href;
const byteStart = length;
const byteEnd = length + getByteLength(text);

return {
index: { byteStart, byteEnd },
features: [
{
$type: "app.bsky.richtext.facet#link",
uri: link,
},
],
};
}

export function getLinkFacets(json: JSONContent) {
const content = json.content;
let length = 0;
let facets: any[] = [];

if (!content) return facets;

content.forEach((p, index) => {
if (index > 0) {
length = length + getByteLength("\n");
}

if (p.content) {
p.content.forEach((item) => {
if (item.marks) {
item.marks.forEach((mark) => {
if (!item.text || mark.type !== "link") return;
facets = [...facets, getFacetFromMark(mark, item.text, length)];
});
}

if (item.type === "hardBreak") {
length = length + getByteLength("\n");
}

if (item.text) {
length = length + getByteLength(item.text);
}

if (item.type === "mention") {
length = length + getByteLength("@" + item.attrs?.id);
}
});
}
});

return facets;
}

export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
Loading

0 comments on commit 2b5ef41

Please sign in to comment.