refactor: centralize icon handling and improve attachment UI

- Move icon name logic to Attachment and Reference classes
- Create reusable setElementIcon Svelte action in ElementHelper
- Update ChatArea and ChatAttachments to use new icon helper
- Add visual separator and background color for message attachments
- Fix duplicate URIs in drag-and-drop file handling
- Improve attachment icon centering with flexbox layout
This commit is contained in:
Andrew Beal 2025-12-23 20:18:27 +00:00
parent dde2120612
commit d2e7b47c65
6 changed files with 119 additions and 53 deletions

View file

@ -11,7 +11,7 @@
import { tick } from "svelte";
import { Selector } from "Enums/Selector";
import { Exception } from "Helpers/Exception";
import { getOuterHeight } from "Helpers/ElementHelper";
import { getOuterHeight, setElementIcon } from "Helpers/ElementHelper";
export let cancelling: boolean = false;
export let messages: ConversationContent[] = [];
@ -263,12 +263,13 @@
{@html content}
</div>
{#if message.references.length > 0}
<hr class="message-attachment-break"/>
<div class="message-attachments-container">
{#each message.references as reference}
<div class="message-attachmanet" aria-label="{reference.fileName}">
<div
class="message-attachment-icon"
use:setElementIcon={reference.getIconName()}
></div>
<div class="message-attachment-info">
<div class="message-attachment-name">{reference.fileName}</div>
@ -430,12 +431,18 @@
}
/* Message attachments styles */
.message-attachment-break {
color: var(--background-secondary-alt);
margin: 0 0 var(--size-4-2) 0;
opacity: 0.5;
}
.message-attachments-container {
display: flex;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
gap: var(--size-4-1);
gap: var(--size-4-2);
margin-bottom: var(--size-4-2);
}
@ -445,8 +452,9 @@
.message-attachmanet {
display: grid;
grid-template-rows: var(--size-4-1) auto var(--size-4-1);
grid-template-columns: var(--size-4-1) auto var(--size-4-1) auto var(--size-4-1);
grid-template-rows: var(--size-4-2) auto var(--size-4-2);
grid-template-columns: var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2);
background-color: var(--background-secondary-alt);
border: var(--border-width) solid var(--background-modifier-border);
border-radius: var(--radius-m);
flex-shrink: 0;
@ -455,7 +463,9 @@
.message-attachment-icon {
grid-row: 2;
grid-column: 2;
align-content: center;
display: flex;
align-items: center;
justify-content: center;
}
.message-attachment-info {

View file

@ -1,20 +1,11 @@
<script lang="ts">
import type { Attachment } from "Conversations/Attachment";
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
import { toMimeType } from "Enums/MimeType";
import { setElementIcon } from "Helpers/ElementHelper";
import { setIcon } from "obsidian";
import {
isAudioFile,
isImageFile,
isKnownFileType,
isTextFile,
isVideoFile,
} from "Enums/FileType";
export let attachments: Attachment[] = [];
let removeAttachmentButtons: (HTMLButtonElement | null)[] = [];
let iconElements: (HTMLDivElement | null)[] = [];
let attachmentElements: (HTMLDivElement | null)[] = [];
let selectedElement: HTMLDivElement | null = null;
@ -26,40 +17,6 @@
});
}
$: if (iconElements.length > 0) {
iconElements.forEach((iconElement, index) => {
if (iconElement && attachments[index]) {
const iconName = getIconName(attachments[index]);
setIcon(iconElement, iconName);
}
});
}
function getIconName(attachment: Attachment | null): string {
if (attachment === null) {
return "file";
}
const fileTypes = MimeTypeToFileTypes[toMimeType(attachment.mimeType)];
if (fileTypes.some((fileType) => isTextFile(fileType))) {
return "file-text";
}
if (fileTypes.some((fileType) => isImageFile(fileType))) {
return "file-image";
}
if (fileTypes.some((fileType) => isAudioFile(fileType))) {
return "file-music";
}
if (fileTypes.some((fileType) => isVideoFile(fileType))) {
return "file-play";
}
if (fileTypes.some((fileType) => isKnownFileType(fileType))) {
return "file";
}
return "file";
}
function removeAttachment(attachment: Attachment) {
attachments = attachments.filter((a) => a !== attachment);
}
@ -95,7 +52,7 @@
bind:this={attachmentElements[index]}>
<div
class="chat-attachment-icon"
bind:this={iconElements[index]}
use:setElementIcon={attachment.getIconName()}
></div>
<div class="chat-attachment-info">
<div class="chat-attachment-name" aria-label="{attachment.fileName}">{attachment.fileName}</div>
@ -131,6 +88,7 @@
display: grid;
grid-template-rows: var(--size-4-2) auto var(--size-4-2);
grid-template-columns: var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2);
background-color: var(--background-secondary-alt);
border: var(--border-width) solid var(--background-modifier-border);
border-radius: var(--radius-m);
max-width: 33%;
@ -145,7 +103,9 @@
.chat-attachment-icon {
grid-row: 2;
grid-column: 2;
align-content: center;
display: flex;
align-items: center;
justify-content: center;
}
.chat-attachment-info {

View file

@ -1,4 +1,7 @@
import type { AIProvider } from "Enums/ApiProvider";
import { isAudioFile, isImageFile, isKnownFileType, isTextFile, isVideoFile } from "Enums/FileType";
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
import { toMimeType } from "Enums/MimeType";
export class Attachment {
@ -43,6 +46,27 @@ export class Attachment {
return Math.round((megaByteSize + Number.EPSILON) * 100) / 100;
}
public getIconName(): string {
const fileTypes = MimeTypeToFileTypes[toMimeType(this.mimeType)];
if (fileTypes.some((fileType) => isTextFile(fileType))) {
return "file-text";
}
if (fileTypes.some((fileType) => isImageFile(fileType))) {
return "file-image";
}
if (fileTypes.some((fileType) => isAudioFile(fileType))) {
return "file-music";
}
if (fileTypes.some((fileType) => isVideoFile(fileType))) {
return "file-play";
}
if (fileTypes.some((fileType) => isKnownFileType(fileType))) {
return "file";
}
return "file";
}
public static isAttachmentData(this: void, data: unknown): data is {
fileName: string;
mimeType: string;

View file

@ -1,3 +1,6 @@
import { isAudioFile, isImageFile, isKnownFileType, isTextFile, isVideoFile } from "Enums/FileType";
import { pathExtname } from "Helpers/Helpers";
export class Reference {
public fileName: string;
@ -8,6 +11,27 @@ export class Reference {
this.size = size;
}
public getIconName(): string {
const extension = pathExtname(this.fileName);
if (isTextFile(extension)) {
return "file-text";
}
if (isImageFile(extension)) {
return "file-image";
}
if (isAudioFile(extension)) {
return "file-music";
}
if (isVideoFile(extension)) {
return "file-play";
}
if (isKnownFileType(extension)) {
return "file";
}
return "file";
}
public static isReferenceData(this: void, data: unknown): data is {
fileName: string;
size: number;

View file

@ -1,5 +1,20 @@
import { setIcon } from "obsidian";
export function getOuterHeight(element: HTMLElement): number {
const marginTop = parseFloat(getComputedStyle(element).marginTop) || 0;
const marginBottom = parseFloat(getComputedStyle(element).marginBottom) || 0;
return element.offsetHeight + marginTop + marginBottom;
}
export function setElementIcon(element: HTMLDivElement, iconName: string) {
if (element) {
setIcon(element, iconName);
}
return {
update(newIconName: string) {
if (element) {
setIcon(element, newIconName);
}
}
};
}

View file

@ -48,9 +48,12 @@ export class InputService {
// get text as sometimes uri's come through as plain text
const textUriList = this.getTextFromDataTransfer(dataTransfer);
const uriList = dataTransfer.getData("text/uri-list");
const uris = `${uriList}\n${textUriList}`.split("\n").unique().map(uri => uri.trim())
const allUris = `${uriList}\n${textUriList}`.split("\n").map(uri => uri.trim())
.filter(uri => uri.length > 0 && !uri.startsWith("#"));
const uris = this.removeDuplicateUris(allUris);
for (const uri of uris) {
try {
const url = new URL(uri);
@ -390,4 +393,34 @@ export class InputService {
return traverse(element);
}
private removeDuplicateUris(uris: string[]): string[] {
const seenFiles = new Set<string>();
const uniqueUris: string[] = [];
for (const uri of uris) {
try {
const url = new URL(uri);
const fileParam = url.searchParams.get("file");
if (fileParam) {
// Normalize by decoding the file parameter to handle different URL encodings
const normalizedFile = decodeURIComponent(fileParam);
if (!seenFiles.has(normalizedFile)) {
seenFiles.add(normalizedFile);
uniqueUris.push(uri);
}
} else {
// If no file parameter, include the URI anyway
uniqueUris.push(uri);
}
} catch {
// If URL parsing fails, include the URI anyway
uniqueUris.push(uri);
}
}
return uniqueUris;
}
}