Add issue embeds

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2023-09-27 16:40:49 +02:00
parent 2864a0311b
commit 1ef0f9dfd1
28 changed files with 585 additions and 107 deletions
+14 -26
View File
@@ -1,11 +1,5 @@
<script lang="ts" strictEvents>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher<{ remove: number; click: null }>();
export let removeable: boolean = false;
export let clickable: boolean = false;
export let key: number;
<script lang="ts">
export let actionable: boolean = false;
</script>
<style>
@@ -13,15 +7,12 @@
user-select: none;
display: inline-flex;
justify-content: center;
align-items: center;
color: var(--color-secondary);
}
.clickable:hover {
cursor: pointer;
background-color: var(--color-secondary-5);
align-items: stretch;
color: inherit;
}
.section {
display: flex;
width: 100%;
align-items: center;
max-width: 13.5rem;
padding: 0.2rem 0.5rem;
@@ -30,8 +21,7 @@
background-color: var(--color-secondary-3);
border-radius: var(--border-radius);
}
.close {
align-self: stretch;
.icon {
color: var(--color-secondary);
border: none;
border-bottom-right-radius: var(--border-radius);
@@ -40,25 +30,23 @@
line-height: 1.5;
cursor: pointer;
}
.close:hover {
.icon:hover {
background-color: var(--color-secondary-5);
color: var(--color-foreground);
}
.removeable {
.actionable {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
</style>
<div class="chip">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="section text" class:removeable class:clickable on:click>
<slot />
<span class="section text" class:actionable>
<slot name="content" />
</span>
{#if removeable}
<button class="section close" on:click={() => dispatch("remove", key)}>
✕
</button>
{#if actionable}
<span class="section icon">
<slot name="icon" />
</span>
{/if}
</div>
+6
View File
@@ -8,6 +8,7 @@
export let text: string;
export let small = false;
export let tiny = false;
export let tooltip: string | undefined = undefined;
const dispatch = createEventDispatcher<{ copied: null }>();
@@ -41,6 +42,10 @@
width: 1.5rem;
height: 1.5rem;
}
.clipboard.tiny {
width: 1rem;
height: 1rem;
}
.clipboard:hover :global(svg) {
fill: var(--color-foreground);
}
@@ -55,6 +60,7 @@
title={tooltip}
class="clipboard"
class:small
class:tiny
on:click|stopPropagation={copy}>
<Icon name={icon} />
</span>
-1
View File
@@ -123,7 +123,6 @@
<div style:margin-top="1rem">
<Reactions
reactions={groupedReactions}
clickable={Boolean($httpdStore.state === "authenticated")}
on:remove={event => {
if (id) {
dispatch("react", { id, ...event.detail });
+35 -1
View File
@@ -8,13 +8,25 @@
import markdown from "@app/lib/markdown";
import { Renderer } from "@app/lib/markdown";
import { highlight } from "@app/lib/syntax";
import { isUrl, twemoji, scrollIntoView, canonicalize } from "@app/lib/utils";
import {
isUrl,
twemoji,
scrollIntoView,
canonicalize,
isCommit,
} from "@app/lib/utils";
import { mimes } from "@app/lib/file";
export let content: string;
// If present, resolve all relative links with respect to this URL
export let linkBaseUrl: string | undefined = undefined;
export let path: string = "/";
export let rawPath: string | undefined = undefined;
// If present, means we are in a preview context,
// use this for image previews instead of /raw URLs.
export let embeds:
| Map<string, { name: string; content: string }>
| undefined = undefined;
$: doc = matter(content);
$: frontMatter = Object.entries(doc.data).filter(
@@ -73,6 +85,28 @@
for (const i of container.querySelectorAll("img")) {
const imagePath = i.getAttribute("src");
// If the image is an oid embed
if (imagePath && isCommit(imagePath)) {
const embed = embeds?.get(imagePath);
if (embed) {
const fileExtension = embed.name.split(".").pop();
if (fileExtension) {
i.setAttribute("src", embed.content);
}
} else {
const fileExtension = i.alt.split(".").pop();
const url = new URL(rawPath);
// If a user changes the alt text of an image,
// the browser is still able to infer the mime type.
if (fileExtension && fileExtension in mimes) {
url.search = `?mime=${mimes[fileExtension]}`;
}
url.pathname = canonicalize(`blobs/${imagePath}`, url.pathname);
i.setAttribute("src", url.toString());
}
continue;
}
// Make sure the source isn't a URL before trying to fetch it from the repo
if (
imagePath &&
+24 -11
View File
@@ -2,9 +2,9 @@
import { createEventDispatcher } from "svelte";
import Chip from "@app/components/Chip.svelte";
import { httpdStore } from "@app/lib/httpd";
export let reactions: Map<string, string[]>;
export let clickable: boolean = false;
const dispatch = createEventDispatcher<{
remove: { nids: string[]; reaction: string };
@@ -22,22 +22,35 @@
flex-direction: row;
gap: 0.5rem;
}
.close {
color: inherit;
border: none;
border-bottom-right-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
background-color: transparent;
line-height: 1.5;
padding: 0;
cursor: pointer;
}
.close:hover {
color: var(--color-foreground);
}
</style>
<div class="reactions">
{#each reactions as [reaction, nids], key}
<Chip
{key}
{clickable}
on:click={() => {
if (clickable) {
dispatch("remove", { nids, reaction });
}
}}>
<div class="reaction txt-tiny">
{#each reactions as [reaction, nids]}
<Chip actionable={Boolean($httpdStore.state === "authenticated")}>
<div slot="content" class="reaction txt-tiny">
<span>{reaction}</span>
<span title={nids.join("\n")}>{nids.length}</span>
</div>
<div slot="icon">
<button
class="close"
on:click={() => dispatch("remove", { nids, reaction })}>
✕
</button>
</div>
</Chip>
{/each}
</div>
+19 -1
View File
@@ -1,11 +1,14 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { afterUpdate, beforeUpdate, createEventDispatcher } from "svelte";
import { isMac } from "@app/lib/utils";
export let resizable: boolean = false;
export let value: string | number | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let focus: boolean = false;
// Defaulting selectionStart and selectionEnd to 0, since no full support yet.
export let selectionStart = 0;
export let selectionEnd = 0;
let textareaElement: HTMLTextAreaElement | undefined = undefined;
@@ -27,6 +30,19 @@
focus = false;
}
beforeUpdate(() => {
if (textareaElement) {
({ selectionStart, selectionEnd } = textareaElement);
}
});
afterUpdate(() => {
if (textareaElement) {
textareaElement.setSelectionRange(selectionStart, selectionEnd);
textareaElement.focus();
}
});
const dispatch = createEventDispatcher<{
submit: null;
}>();
@@ -95,12 +111,14 @@
<textarea
bind:this={textareaElement}
bind:value
aria-label="textarea-comment"
class="txt-small"
class:resizable
{placeholder}
on:change
on:click
on:input
on:drop
on:keydown|stopPropagation={handleKeydown}
on:keypress />
+33 -2
View File
@@ -7,13 +7,35 @@
import { createEventDispatcher, tick } from "svelte";
import { scrollIntoView } from "@app/lib/utils";
import { httpdStore } from "@app/lib/httpd";
import { embed } from "@app/lib/file";
export let newEmbeds: { name: string; content: string }[] = [];
export let selectionStart = 0;
export let selectionEnd = 0;
export let thread: { root: Comment; replies: Comment[] };
export let rawPath: string;
export let showReplyTextarea = false;
let replyText = "";
function handleFileDrop(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) {
const embeds = Array.from(event.dataTransfer.files).map(embed);
void Promise.all(embeds).then(embeds =>
embeds.forEach(embed => {
newEmbeds.push({ name: embed.name, content: embed.content });
const embedText = `![${embed.name}](${embed.oid})\n`;
replyText = replyText
.slice(0, selectionStart)
.concat(embedText, replyText.slice(selectionEnd));
selectionStart += embedText.length;
selectionEnd = selectionStart;
}),
);
}
}
function cancel() {
showReplyTextarea = false;
scrollIntoView(root.id, {
@@ -36,12 +58,18 @@
}
function reply() {
dispatch("reply", { id: root.id, body: replyText });
dispatch("reply", { id: root.id, embeds: newEmbeds, body: replyText });
replyText = "";
newEmbeds = [];
showReplyTextarea = false;
}
const dispatch = createEventDispatcher<{
reply: { id: string; body: string };
reply: {
id: string;
embeds: { name: string; content: string }[];
body: string;
};
react: { nids: string[]; commentId: string | undefined; reaction: string };
cancel: never;
}>();
@@ -106,6 +134,9 @@
focus={showReplyTextarea}
bind:value={replyText}
on:submit={reply}
on:drop={handleFileDrop}
bind:selectionStart
bind:selectionEnd
placeholder="Leave your reply" />
<div class="actions">
<Button variant="text" size="small" on:click={cancel}>Dismiss</Button>