* Replace quick capture * Transfer to inbox * Fix inbox edit * fixup! Fix inbox edit * fixup! fixup! Fix inbox edit * fixup! fixup! fixup! Fix inbox edit * fixup! fixup! fixup! fixup! Fix inbox edit * fixup! fixup! fixup! fixup! fixup! Fix inbox edit * Add long text * fixup! Add long text * fixup! fixup! Add long text * fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text
730 lines
27 KiB
TypeScript
730 lines
27 KiB
TypeScript
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { InboxItem } from '../../entities/InboxItem';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
TrashIcon,
|
|
DocumentTextIcon,
|
|
FolderIcon,
|
|
ClipboardDocumentListIcon,
|
|
TagIcon,
|
|
GlobeAltIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { Task } from '../../entities/Task';
|
|
import { Project } from '../../entities/Project';
|
|
import { Note } from '../../entities/Note';
|
|
import ConfirmDialog from '../Shared/ConfirmDialog';
|
|
import { useStore } from '../../store/useStore';
|
|
import QuickCaptureInput, {
|
|
InboxComposerFooterContext,
|
|
QuickCaptureInputHandle,
|
|
} from './QuickCaptureInput';
|
|
import InboxCard from './InboxCard';
|
|
import { isUrl } from '../../utils/urlService';
|
|
|
|
interface InboxItemDetailProps {
|
|
item: InboxItem;
|
|
onDelete: (uid: string) => void;
|
|
onUpdate?: (uid: string, newContent: string) => Promise<void>;
|
|
openTaskModal: (task: Task, inboxItemUid?: string) => void;
|
|
openProjectModal: (project: Project | null, inboxItemUid?: string) => void;
|
|
openNoteModal: (note: Note | null, inboxItemUid?: string) => void;
|
|
projects: Project[];
|
|
}
|
|
|
|
const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
|
|
item,
|
|
onDelete,
|
|
onUpdate,
|
|
openTaskModal,
|
|
openProjectModal,
|
|
openNoteModal,
|
|
projects,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const {
|
|
tagsStore: { tags },
|
|
} = useStore();
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const composerRef = useRef<QuickCaptureInputHandle>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isEditing) {
|
|
return;
|
|
}
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
containerRef.current &&
|
|
!containerRef.current.contains(event.target as Node)
|
|
) {
|
|
if (composerRef.current) {
|
|
void composerRef.current.submit();
|
|
} else {
|
|
setIsEditing(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
setIsEditing(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [isEditing]);
|
|
|
|
const parseHashtags = (text: string): string[] => {
|
|
const trimmedText = text.trim();
|
|
const matches: string[] = [];
|
|
|
|
const words = trimmedText.split(/\s+/);
|
|
if (words.length === 0) return matches;
|
|
|
|
let i = 0;
|
|
while (i < words.length) {
|
|
if (words[i].startsWith('#') || words[i].startsWith('+')) {
|
|
let groupEnd = i;
|
|
while (
|
|
groupEnd < words.length &&
|
|
(words[groupEnd].startsWith('#') ||
|
|
words[groupEnd].startsWith('+'))
|
|
) {
|
|
groupEnd++;
|
|
}
|
|
|
|
for (let j = i; j < groupEnd; j++) {
|
|
if (words[j].startsWith('#')) {
|
|
const tagName = words[j].substring(1);
|
|
if (
|
|
tagName &&
|
|
/^[a-zA-Z0-9_-]+$/.test(tagName) &&
|
|
!matches.includes(tagName)
|
|
) {
|
|
matches.push(tagName);
|
|
}
|
|
}
|
|
}
|
|
|
|
i = groupEnd;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
};
|
|
|
|
const parseProjectRefs = (text: string): string[] => {
|
|
const trimmedText = text.trim();
|
|
const matches: string[] = [];
|
|
|
|
const tokens = tokenizeText(trimmedText);
|
|
|
|
let i = 0;
|
|
while (i < tokens.length) {
|
|
if (tokens[i].startsWith('#') || tokens[i].startsWith('+')) {
|
|
let groupEnd = i;
|
|
while (
|
|
groupEnd < tokens.length &&
|
|
(tokens[groupEnd].startsWith('#') ||
|
|
tokens[groupEnd].startsWith('+'))
|
|
) {
|
|
groupEnd++;
|
|
}
|
|
|
|
for (let j = i; j < groupEnd; j++) {
|
|
if (tokens[j].startsWith('+')) {
|
|
let projectName = tokens[j].substring(1);
|
|
|
|
if (
|
|
projectName.startsWith('"') &&
|
|
projectName.endsWith('"')
|
|
) {
|
|
projectName = projectName.slice(1, -1);
|
|
}
|
|
|
|
if (projectName && !matches.includes(projectName)) {
|
|
matches.push(projectName);
|
|
}
|
|
}
|
|
}
|
|
|
|
i = groupEnd;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
};
|
|
|
|
const tokenizeText = (text: string): string[] => {
|
|
const tokens: string[] = [];
|
|
let currentToken = '';
|
|
let inQuotes = false;
|
|
let i = 0;
|
|
|
|
while (i < text.length) {
|
|
const char = text[i];
|
|
|
|
if (char === '"' && (i === 0 || text[i - 1] === '+')) {
|
|
inQuotes = true;
|
|
currentToken += char;
|
|
} else if (char === '"' && inQuotes) {
|
|
inQuotes = false;
|
|
currentToken += char;
|
|
} else if (char === ' ' && !inQuotes) {
|
|
if (currentToken) {
|
|
tokens.push(currentToken);
|
|
currentToken = '';
|
|
}
|
|
} else {
|
|
currentToken += char;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
if (currentToken) {
|
|
tokens.push(currentToken);
|
|
}
|
|
|
|
return tokens;
|
|
};
|
|
|
|
const cleanTextFromTagsAndProjects = (text: string): string => {
|
|
const trimmedText = text.trim();
|
|
const tokens = tokenizeText(trimmedText);
|
|
const cleanedTokens: string[] = [];
|
|
|
|
let i = 0;
|
|
while (i < tokens.length) {
|
|
if (tokens[i].startsWith('#') || tokens[i].startsWith('+')) {
|
|
while (
|
|
i < tokens.length &&
|
|
(tokens[i].startsWith('#') || tokens[i].startsWith('+'))
|
|
) {
|
|
i++;
|
|
}
|
|
} else {
|
|
cleanedTokens.push(tokens[i]);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return cleanedTokens.join(' ').trim();
|
|
};
|
|
|
|
const fullContent = item.content || '';
|
|
const displayText =
|
|
item.title && item.title.trim().length > 0 ? item.title : fullContent;
|
|
const baseContent = fullContent || displayText;
|
|
const cleanedPreviewText = cleanTextFromTagsAndProjects(displayText);
|
|
const previewText =
|
|
cleanedPreviewText.length > 0 ? cleanedPreviewText : displayText;
|
|
|
|
const hashtags = useMemo(() => {
|
|
const parsed = parseHashtags(fullContent);
|
|
const hasBookmark = parsed.some(
|
|
(tag) => tag.toLowerCase() === 'bookmark'
|
|
);
|
|
if (!hasBookmark && isUrl(fullContent.trim())) {
|
|
return [...parsed, 'bookmark'];
|
|
}
|
|
return parsed;
|
|
}, [fullContent]);
|
|
const isBookmarkItem = useMemo(
|
|
() => hashtags.some((tag) => tag.toLowerCase() === 'bookmark'),
|
|
[hashtags]
|
|
);
|
|
const projectRefs = parseProjectRefs(fullContent);
|
|
const hasLongContent =
|
|
Boolean(item.title && item.title.trim()) &&
|
|
item.title !== null &&
|
|
item.title !== fullContent;
|
|
const iconTooltip = isBookmarkItem
|
|
? t('inbox.iconTooltip.bookmark', 'Bookmark link')
|
|
: t('inbox.iconTooltip.text', 'Captured text');
|
|
|
|
const slugify = (text: string) =>
|
|
text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
|
|
const getTagLink = (tagName: string) => {
|
|
const tag = tags.find(
|
|
(t) => t.name.toLowerCase() === tagName.toLowerCase()
|
|
);
|
|
if (tag?.uid) {
|
|
return `/tag/${tag.uid}-${slugify(tag.name)}`;
|
|
}
|
|
return `/tag/${encodeURIComponent(tagName)}`;
|
|
};
|
|
|
|
const linkifyContent = (text: string): React.ReactNode => {
|
|
const urlRegex = /(https?:\/\/[^\s]+)/gi;
|
|
const matches = [...text.matchAll(urlRegex)];
|
|
|
|
if (matches.length === 0) {
|
|
return text;
|
|
}
|
|
|
|
const nodes: React.ReactNode[] = [];
|
|
let lastIndex = 0;
|
|
|
|
matches.forEach((match, idx) => {
|
|
const start = match.index ?? 0;
|
|
const url = match[0];
|
|
if (start > lastIndex) {
|
|
nodes.push(
|
|
<React.Fragment key={`text-${idx}-${start}`}>
|
|
{text.slice(lastIndex, start)}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
nodes.push(
|
|
<a
|
|
key={`url-${idx}-${start}`}
|
|
href={url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 dark:text-blue-400 underline-offset-2 hover:underline break-all"
|
|
>
|
|
{url}
|
|
</a>
|
|
);
|
|
lastIndex = start + url.length;
|
|
});
|
|
|
|
if (lastIndex < text.length) {
|
|
nodes.push(
|
|
<React.Fragment key={`text-tail-${lastIndex}`}>
|
|
{text.slice(lastIndex)}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
return nodes;
|
|
};
|
|
|
|
const buildConversionPayload = (
|
|
textOverride?: string,
|
|
hashtagOverride?: string[],
|
|
projectRefsOverride?: string[],
|
|
cleanedOverride?: string
|
|
) => {
|
|
const sourceText = textOverride ?? baseContent;
|
|
const sourceHashtags = hashtagOverride ?? parseHashtags(sourceText);
|
|
const sourceProjectRefs =
|
|
projectRefsOverride ?? parseProjectRefs(sourceText);
|
|
const cleaned =
|
|
cleanedOverride ??
|
|
cleanTextFromTagsAndProjects(sourceText) ??
|
|
sourceText;
|
|
|
|
const tagObjects = sourceHashtags.map((hashtagName) => {
|
|
const existingTag = tags.find(
|
|
(tag) => tag.name.toLowerCase() === hashtagName.toLowerCase()
|
|
);
|
|
return existingTag || { name: hashtagName };
|
|
});
|
|
|
|
let projectId = undefined;
|
|
if (sourceProjectRefs.length > 0) {
|
|
const projectName = sourceProjectRefs[0];
|
|
const matchingProject = projects.find(
|
|
(project) =>
|
|
project.name.toLowerCase() === projectName.toLowerCase()
|
|
);
|
|
if (matchingProject) {
|
|
projectId = matchingProject.id;
|
|
}
|
|
}
|
|
|
|
return {
|
|
sourceText,
|
|
cleanedContent: cleaned,
|
|
tagObjects,
|
|
projectId,
|
|
projectRefsList: sourceProjectRefs,
|
|
hashtagsList: sourceHashtags,
|
|
};
|
|
};
|
|
|
|
const handleConvertToTask = (context?: InboxComposerFooterContext) => {
|
|
try {
|
|
const payload = buildConversionPayload(
|
|
context?.text,
|
|
context?.hashtags,
|
|
context?.projectRefs,
|
|
context?.cleanedText
|
|
);
|
|
|
|
const newTask: Task = {
|
|
name: payload.cleanedContent || displayText,
|
|
status: 'not_started',
|
|
priority: null,
|
|
tags: payload.tagObjects,
|
|
project_id: payload.projectId,
|
|
completed_at: null,
|
|
};
|
|
|
|
if (item.uid !== undefined) {
|
|
openTaskModal(newTask, item.uid);
|
|
} else {
|
|
openTaskModal(newTask);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error converting to task:', error);
|
|
}
|
|
};
|
|
|
|
const handleSubmitEdit = async (text: string) => {
|
|
if (!onUpdate || item.uid === undefined) {
|
|
return;
|
|
}
|
|
|
|
const trimmedCurrent = baseContent.trim();
|
|
const trimmedNew = text.trim();
|
|
|
|
if (trimmedCurrent === trimmedNew) {
|
|
setIsEditing(false);
|
|
return;
|
|
}
|
|
|
|
await onUpdate(item.uid, text);
|
|
};
|
|
|
|
const handleConvertToProject = (context?: InboxComposerFooterContext) => {
|
|
try {
|
|
const payload = buildConversionPayload(
|
|
context?.text,
|
|
context?.hashtags,
|
|
context?.projectRefs,
|
|
context?.cleanedText
|
|
);
|
|
|
|
const newProject: Project = {
|
|
name: payload.cleanedContent || displayText,
|
|
description: '',
|
|
state: 'planned',
|
|
tags: payload.tagObjects,
|
|
};
|
|
|
|
if (item.uid !== undefined) {
|
|
openProjectModal(newProject, item.uid);
|
|
} else {
|
|
openProjectModal(newProject);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error converting to project:', error);
|
|
}
|
|
};
|
|
|
|
const handleConvertToNote = async (
|
|
context?: InboxComposerFooterContext
|
|
) => {
|
|
const sourceText = context?.text ?? baseContent;
|
|
let title = sourceText.split('\n')[0] || sourceText.substring(0, 50);
|
|
let content = sourceText;
|
|
let isBookmark = false;
|
|
|
|
try {
|
|
const { isUrl: detectUrl, extractUrlTitle } = await import(
|
|
'../../utils/urlService'
|
|
);
|
|
|
|
if (detectUrl(sourceText.trim())) {
|
|
setLoading(true);
|
|
try {
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Timeout')), 10000)
|
|
);
|
|
|
|
const result = (await Promise.race([
|
|
extractUrlTitle(sourceText.trim()),
|
|
timeoutPromise,
|
|
])) as any;
|
|
|
|
if (result && result.title) {
|
|
title = result.title;
|
|
content = sourceText;
|
|
isBookmark = true;
|
|
}
|
|
} catch (titleError) {
|
|
console.error('Error extracting URL title:', titleError);
|
|
isBookmark = true;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking URL or extracting title:', error);
|
|
setLoading(false);
|
|
}
|
|
|
|
const payload = buildConversionPayload(
|
|
context?.text,
|
|
context?.hashtags,
|
|
context?.projectRefs,
|
|
context?.cleanedText
|
|
);
|
|
|
|
const bookmarkTag = isBookmark ? [{ name: 'bookmark' }] : [];
|
|
const tagObjects = [...payload.tagObjects, ...bookmarkTag];
|
|
|
|
const finalTitle =
|
|
title === content ? payload.cleanedContent || sourceText : title;
|
|
const finalContent = payload.cleanedContent || sourceText;
|
|
|
|
const newNote: Note = {
|
|
title: finalTitle,
|
|
content: finalContent,
|
|
tags: tagObjects,
|
|
project_uid: payload.projectId,
|
|
};
|
|
|
|
if (item.uid !== undefined) {
|
|
openNoteModal(newNote, item.uid);
|
|
} else {
|
|
openNoteModal(newNote);
|
|
}
|
|
};
|
|
|
|
const renderComposerFooter = (context: InboxComposerFooterContext) => (
|
|
<div className="pt-3 mt-3 border-t border-gray-100 dark:border-gray-800">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{loading && <div className="spinner h-4 w-4" />}
|
|
<button
|
|
onClick={() => handleConvertToTask(context)}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-200 bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/40 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-200 dark:focus:ring-offset-gray-900"
|
|
>
|
|
<span className="flex items-center gap-1">
|
|
<span className="sm:hidden text-sm font-semibold leading-none">
|
|
+
|
|
</span>
|
|
<ClipboardDocumentListIcon className="h-4 w-4" />
|
|
<span className="hidden sm:inline">
|
|
{t('inbox.createTask', 'Task')}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
<button
|
|
onClick={() => handleConvertToNote(context)}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-purple-700 dark:text-purple-200 bg-purple-50 dark:bg-purple-900/20 border border-purple-100 dark:border-purple-800 rounded-md hover:bg-purple-100 dark:hover:bg-purple-900/40 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-200 dark:focus:ring-offset-gray-900"
|
|
>
|
|
<span className="flex items-center gap-1">
|
|
<span className="sm:hidden text-sm font-semibold leading-none">
|
|
+
|
|
</span>
|
|
<DocumentTextIcon className="h-4 w-4" />
|
|
<span className="hidden sm:inline">
|
|
{t('inbox.createNote', 'Note')}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
<button
|
|
onClick={() => handleConvertToProject(context)}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-green-700 dark:text-green-200 bg-green-50 dark:bg-green-900/20 border border-green-100 dark:border-green-800 rounded-md hover:bg-green-100 dark:hover:bg-green-900/40 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-200 dark:focus:ring-offset-gray-900"
|
|
>
|
|
<span className="flex items-center gap-1">
|
|
<span className="sm:hidden text-sm font-semibold leading-none">
|
|
+
|
|
</span>
|
|
<FolderIcon className="h-4 w-4" />
|
|
<span className="hidden sm:inline">
|
|
{t('inbox.createProject', 'Project')}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
onClick={handleDelete}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-300 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded-md hover:bg-red-100 dark:hover:bg-red-900/40 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-200 dark:focus:ring-offset-gray-900"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
<span className="hidden sm:inline">
|
|
{t('common.delete', 'Delete')}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const handleDelete = () => {
|
|
setShowConfirmDialog(true);
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (item.uid !== undefined) {
|
|
onDelete(item.uid);
|
|
}
|
|
setShowConfirmDialog(false);
|
|
};
|
|
|
|
const handleStartEdit = () => {
|
|
if (!isEditing) {
|
|
setIsEditing(true);
|
|
}
|
|
};
|
|
|
|
const renderMetadata = () =>
|
|
(hashtags.length > 0 || projectRefs.length > 0) && (
|
|
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 mt-1 ml-8">
|
|
{projectRefs.length > 0 && (
|
|
<div className="flex items-center">
|
|
<FolderIcon className="h-3 w-3 mr-1" />
|
|
<span>
|
|
{projectRefs.map((projectRef, index) => {
|
|
const matchingProject = projects.find(
|
|
(project) =>
|
|
project.name.toLowerCase() ===
|
|
projectRef.toLowerCase()
|
|
);
|
|
|
|
if (matchingProject) {
|
|
return (
|
|
<React.Fragment key={projectRef}>
|
|
<Link
|
|
to={
|
|
matchingProject.uid
|
|
? `/project/${matchingProject.uid}-${matchingProject.name
|
|
.toLowerCase()
|
|
.replace(
|
|
/[^a-z0-9]+/g,
|
|
'-'
|
|
)
|
|
.replace(
|
|
/^-|-$/g,
|
|
''
|
|
)}`
|
|
: `/project/${matchingProject.id}`
|
|
}
|
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
|
>
|
|
{projectRef}
|
|
</Link>
|
|
{index < projectRefs.length - 1 &&
|
|
', '}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<React.Fragment key={projectRef}>
|
|
<span>{projectRef}</span>
|
|
{index < projectRefs.length - 1 && ', '}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{projectRefs.length > 0 && hashtags.length > 0 && (
|
|
<span className="mx-2">•</span>
|
|
)}
|
|
|
|
{hashtags.length > 0 && (
|
|
<div className="flex items-center">
|
|
<TagIcon className="h-3 w-3 mr-1" />
|
|
<span>
|
|
{hashtags.map((hashtag, index) => (
|
|
<React.Fragment key={hashtag}>
|
|
<Link
|
|
to={getTagLink(hashtag)}
|
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
|
>
|
|
{hashtag}
|
|
</Link>
|
|
{index < hashtags.length - 1 && ', '}
|
|
</React.Fragment>
|
|
))}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div ref={containerRef}>
|
|
{isEditing ? (
|
|
<QuickCaptureInput
|
|
ref={composerRef}
|
|
mode="edit"
|
|
initialValue={fullContent}
|
|
hidePrimaryButton
|
|
projects={projects}
|
|
onSubmitOverride={handleSubmitEdit}
|
|
onAfterSubmit={() => setIsEditing(false)}
|
|
renderFooterActions={renderComposerFooter}
|
|
openTaskModal={openTaskModal}
|
|
openProjectModal={openProjectModal}
|
|
openNoteModal={openNoteModal}
|
|
cardClassName="mb-0"
|
|
multiline={hasLongContent}
|
|
/>
|
|
) : (
|
|
<InboxCard className="w-full">
|
|
<div className="px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="flex-shrink-0"
|
|
title={iconTooltip}
|
|
aria-label={iconTooltip}
|
|
>
|
|
{isBookmarkItem ? (
|
|
<GlobeAltIcon className="h-5 w-5 text-blue-500 dark:text-blue-300" />
|
|
) : (
|
|
<DocumentTextIcon
|
|
className={`h-5 w-5 ${
|
|
hasLongContent
|
|
? 'text-purple-500 dark:text-purple-300'
|
|
: 'text-gray-400 dark:text-gray-500'
|
|
}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<button
|
|
onClick={handleStartEdit}
|
|
className="text-base font-medium text-gray-900 dark:text-gray-300 break-words text-left cursor-pointer w-full hover:text-blue-600 dark:hover:text-blue-400"
|
|
>
|
|
{linkifyContent(previewText)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{renderMetadata()}
|
|
</div>
|
|
</InboxCard>
|
|
)}
|
|
{showConfirmDialog && (
|
|
<ConfirmDialog
|
|
title={t('inbox.deleteConfirmTitle', 'Delete Item')}
|
|
message={t(
|
|
'inbox.deleteConfirmMessage',
|
|
'Are you sure you want to delete this inbox item? This action cannot be undone.'
|
|
)}
|
|
onConfirm={confirmDelete}
|
|
onCancel={() => setShowConfirmDialog(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default InboxItemDetail;
|