sojorn/sojorn_app/lib/services/markdown_converter.dart
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**Major Features Added:**
- **Inline Reply System**: Replace compose screen with inline reply boxes
- **Thread Navigation**: Parent/child navigation with jump functionality
- **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy
- **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions

 **Frontend Changes:**
- **ThreadedCommentWidget**: Complete rewrite with animations and navigation
- **ThreadNode Model**: Added parent references and descendant counting
- **ThreadedConversationScreen**: Integrated navigation handlers
- **PostDetailScreen**: Replaced with threaded conversation view
- **ComposeScreen**: Added reply indicators and context
- **PostActions**: Fixed visibility checks for chain buttons

 **Backend Changes:**
- **API Route**: Added /posts/:id/thread endpoint
- **Post Repository**: Include allow_chain and visibility fields in feed
- **Thread Handler**: Support for fetching post chains

 **UI/UX Improvements:**
- **Reply Context**: Clear indication when replying to specific posts
- **Character Counting**: 500 character limit with live counter
- **Visual Hierarchy**: Depth-based indentation and styling
- **Smooth Animations**: SizeTransition, FadeTransition, hover states
- **Chain Navigation**: Parent/child buttons with visual feedback

 **Technical Enhancements:**
- **Animation Controllers**: Proper lifecycle management
- **State Management**: Clean separation of concerns
- **Navigation Callbacks**: Reusable navigation system
- **Error Handling**: Graceful fallbacks and user feedback

This creates a Reddit-style threaded conversation experience with smooth
animations, inline replies, and intuitive navigation between posts in a chain.
2026-01-30 07:40:19 -06:00

143 lines
3.9 KiB
Dart

import 'package:flutter_quill/flutter_quill.dart' as quill;
/// Service for converting between Quill Delta format and Markdown
///
/// Handles bidirectional conversion:
/// - Quill Delta → Markdown (for storage)
/// - Markdown → Quill Delta (for editing existing posts)
class MarkdownConverter {
/// Convert Quill document to Markdown string
String toMarkdown(quill.Document document) {
final buffer = StringBuffer();
final delta = document.toDelta();
for (final op in delta.toList()) {
if (op.data is! String) continue;
String text = op.data as String;
final attrs = op.attributes;
if (attrs == null) {
buffer.write(text);
continue;
}
// Handle block-level formatting
if (attrs.containsKey('header')) {
final level = attrs['header'] as int;
final hashes = '#' * level;
buffer.write('$hashes $text');
continue;
}
if (attrs.containsKey('blockquote')) {
buffer.write('> $text');
continue;
}
if (attrs.containsKey('code-block')) {
buffer.write('```\n$text```\n');
continue;
}
if (attrs.containsKey('list')) {
final listType = attrs['list'];
if (listType == 'bullet') {
buffer.write('- $text');
} else if (listType == 'ordered') {
buffer.write('1. $text');
}
continue;
}
// Handle inline formatting
String formattedText = text;
if (attrs.containsKey('bold')) {
formattedText = '**$formattedText**';
}
if (attrs.containsKey('italic')) {
formattedText = '_${formattedText}_';
}
if (attrs.containsKey('underline')) {
// Markdown doesn't have native underline, use HTML
formattedText = '<u>$formattedText</u>';
}
if (attrs.containsKey('strike')) {
formattedText = '~~$formattedText~~';
}
if (attrs.containsKey('code')) {
formattedText = '`$formattedText`';
}
if (attrs.containsKey('link')) {
final url = attrs['link'];
formattedText = '[$formattedText]($url)';
}
buffer.write(formattedText);
}
return buffer.toString();
}
/// Convert Markdown string to Quill document
///
/// Note: This is a simplified implementation. For production use,
/// consider using a proper Markdown parser or the quill_markdown package.
quill.Document fromMarkdown(String markdown) {
// For now, return a simple document with the markdown as plain text
// This will be enhanced in future iterations
return quill.Document()..insert(0, markdown);
}
/// Get character count of rendered text (without Markdown syntax)
int getRenderedCharCount(quill.Document document) {
final markdown = toMarkdown(document);
final rendered = _stripMarkdownSyntax(markdown);
return rendered.length;
}
/// Strip Markdown syntax to get plain text character count
String _stripMarkdownSyntax(String markdown) {
String text = markdown;
// Remove headers
text = text.replaceAll(RegExp(r'^#{1,6}\s+', multiLine: true), '');
// Remove bold
text = text.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1');
// Remove italic
text = text.replaceAll(RegExp(r'_(.+?)_'), r'$1');
// Remove strikethrough
text = text.replaceAll(RegExp(r'~~(.+?)~~'), r'$1');
// Remove inline code
text = text.replaceAll(RegExp(r'`(.+?)`'), r'$1');
// Remove links but keep text
text = text.replaceAll(RegExp(r'\[(.+?)\]\(.+?\)'), r'$1');
// Remove blockquotes
text = text.replaceAll(RegExp(r'^>\s+', multiLine: true), '');
// Remove list markers
text = text.replaceAll(RegExp(r'^[-*]\s+', multiLine: true), '');
text = text.replaceAll(RegExp(r'^\d+\.\s+', multiLine: true), '');
// Remove HTML tags (for underline)
text = text.replaceAll(RegExp(r'<[^>]+>'), '');
// Remove code block markers
text = text.replaceAll('```', '');
return text;
}
}