mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-13 18:40:50 +08:00
+14
-26
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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 = `\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>
|
||||
|
||||
Reference in New Issue
Block a user