Skip to content

Commit

Permalink
feat: post markdown content (#692)
Browse files Browse the repository at this point in the history
## Description
This PR adds support for markdown / quill delta for regular feed posts

## Type of Change
- [x] Bug fix
- [x] New feature
- [ ] Breaking change
- [ ] Refactoring
- [ ] Documentation
- [ ] Chore

## Screenshots (if applicable)
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/378bda29-bf58-42e3-9a26-8d97a16ff3c6"
/>
  • Loading branch information
ice-orion authored and ice-hector committed Feb 13, 2025
1 parent 0a3e202 commit 6f43f1d
Show file tree
Hide file tree
Showing 22 changed files with 207 additions and 251 deletions.
4 changes: 2 additions & 2 deletions lib/app/components/text_editor/text_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:ion/app/components/text_editor/components/custom_blocks/text_edi
import 'package:ion/app/components/text_editor/components/custom_blocks/text_editor_separator_block/text_editor_separator_block.dart';
import 'package:ion/app/components/text_editor/components/custom_blocks/text_editor_single_image_block/text_editor_single_image_block.dart';
import 'package:ion/app/components/text_editor/utils/mentions_hashtags_handler.dart';
import 'package:ion/app/components/text_editor/utils/quill.dart';
import 'package:ion/app/components/text_editor/utils/text_editor_styles.dart';

class TextEditor extends ConsumerStatefulWidget {
TextEditor(
Expand Down Expand Up @@ -71,7 +71,7 @@ class TextEditorState extends ConsumerState<TextEditor> {
],
autoFocus: widget.autoFocus,
placeholder: widget.placeholder,
customStyles: getCustomStyles(context),
customStyles: textEditorStyles(context),
floatingCursorDisabled: true,
customStyleBuilder: (attribute) => customTextStyleBuilder(attribute, context),
),
Expand Down
13 changes: 10 additions & 3 deletions lib/app/components/text_editor/text_editor_preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ import 'package:flutter_quill/quill_delta.dart';
import 'package:ion/app/components/text_editor/components/custom_blocks/text_editor_code_block/text_editor_code_block.dart';
import 'package:ion/app/components/text_editor/components/custom_blocks/text_editor_separator_block/text_editor_separator_block.dart';
import 'package:ion/app/components/text_editor/components/custom_blocks/text_editor_single_image_block/text_editor_single_image_block.dart';
import 'package:ion/app/components/text_editor/utils/quill.dart';
import 'package:ion/app/components/text_editor/utils/text_editor_styles.dart';
import 'package:ion/app/features/ion_connect/model/media_attachment.dart';

class TextEditorPreview extends HookWidget {
const TextEditorPreview({
required this.content,
this.enableInteractiveSelection = false,
this.media,
this.maxHeight,
this.customStyles,
super.key,
});

final Delta content;
final bool enableInteractiveSelection;
final Map<String, MediaAttachment>? media;
final DefaultStyles? customStyles;
final double? maxHeight;

@override
Widget build(BuildContext context) {
Expand All @@ -35,8 +41,9 @@ class TextEditorPreview extends HookWidget {
enableSelectionToolbar: false,
floatingCursorDisabled: true,
showCursor: false,
enableInteractiveSelection: false,
customStyles: getCustomStyles(context),
enableInteractiveSelection: enableInteractiveSelection,
customStyles: customStyles ?? textEditorStyles(context),
maxHeight: maxHeight,
embedBuilders: [
TextEditorSingleImageBuilder(media: media),
TextEditorSeparatorBuilder(),
Expand Down
12 changes: 12 additions & 0 deletions lib/app/components/text_editor/utils/build_empty_delta.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: ice License 1.0

import 'package:flutter_quill/quill_delta.dart';

/// Creates new empty delta.
/// All Quill documents must end with a newline character,
/// even if there is no formatting applied to the last line.
///
/// https://quilljs.com/docs/delta#line-formatting
Delta buildEmptyDelta() {
return Delta()..insert('\n');
}
10 changes: 10 additions & 0 deletions lib/app/components/text_editor/utils/is_attributed_operation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: ice License 1.0

import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';

bool isAttributedOperation(Operation operation, {required Attribute<dynamic> attribute}) {
final attributes = operation.attributes;
if (attributes == null) return false;
return attributes.containsKey(attribute.key);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import 'package:flutter_quill/flutter_quill.dart';
import 'package:ion/app/components/text_editor/attributes.dart';
import 'package:ion/app/extensions/extensions.dart';

DefaultStyles getCustomStyles(BuildContext context) {
DefaultStyles textEditorStyles(BuildContext context, {Color? color}) {
final textColor = color ?? context.theme.appColors.secondaryText;
return DefaultStyles(
paragraph: DefaultTextBlockStyle(
context.theme.appTextThemes.body2.copyWith(
color: context.theme.appColors.secondaryText,
color: textColor,
),
HorizontalSpacing.zero,
VerticalSpacing.zero,
Expand All @@ -18,11 +19,11 @@ DefaultStyles getCustomStyles(BuildContext context) {
),
bold: context.theme.appTextThemes.body2.copyWith(
fontWeight: FontWeight.bold,
color: context.theme.appColors.secondaryText,
color: textColor,
),
italic: context.theme.appTextThemes.body2.copyWith(
fontStyle: FontStyle.italic,
color: context.theme.appColors.secondaryText,
color: textColor,
),
placeHolder: DefaultTextBlockStyle(
context.theme.appTextThemes.body2.copyWith(
Expand All @@ -44,7 +45,7 @@ DefaultStyles getCustomStyles(BuildContext context) {
),
lists: DefaultListBlockStyle(
context.theme.appTextThemes.body2.copyWith(
color: context.theme.appColors.secondaryText,
color: textColor,
fontSize: context.theme.appTextThemes.body2.fontSize,
),
HorizontalSpacing.zero,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_quill/quill_delta.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ion/app/components/button/button.dart';
import 'package:ion/app/components/screen_offset/screen_side_offset.dart';
Expand Down Expand Up @@ -144,8 +145,8 @@ class _ActionButton extends HookConsumerWidget {
return MessagingBottomBar(
onSubmitted: (content) async {
await ref.read(createPostNotifierProvider(CreatePostOption.community).notifier).create(
content: content ?? '',
communtiyId: communityId,
content: content != null ? (Delta()..insert('$content\n')) : null,
communityId: communityId,
);
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import 'package:ion/app/features/ion_connect/model/entity_data_with_media_conten
import 'package:ion/app/features/ion_connect/model/media_attachment.dart';
import 'package:ion/app/features/ion_connect/model/related_event.c.dart';
import 'package:ion/app/features/ion_connect/model/related_pubkey.c.dart';
import 'package:ion/app/services/text_parser/model/text_match.c.dart';
import 'package:ion/app/services/text_parser/text_parser.dart';
import 'package:ion/app/services/uuid/uuid.dart';

part 'private_direct_message_data.c.freezed.dart';
Expand Down Expand Up @@ -66,7 +64,7 @@ class PrivateDirectMessageEntity with _$PrivateDirectMessageEntity {
@freezed
class PrivateDirectMessageData with _$PrivateDirectMessageData, EntityDataWithMediaContent {
const factory PrivateDirectMessageData({
required List<TextMatch> content,
required String content,
required Map<String, MediaAttachment> media,
required String uuid,
String? relatedGroupImagePath,
Expand All @@ -77,12 +75,10 @@ class PrivateDirectMessageData with _$PrivateDirectMessageData, EntityDataWithMe
}) = _PrivateDirectMessageData;

factory PrivateDirectMessageData.fromEventMessage(EventMessage eventMessage) {
final parsedContent = TextParser.allMatchers().parse(eventMessage.content);

final tags = groupBy(eventMessage.tags, (tag) => tag[0]);

return PrivateDirectMessageData(
content: parsedContent,
content: eventMessage.content,
media: EntityDataWithMediaContent.parseImeta(tags[MediaAttachment.tagName]),
relatedSubject: tags[RelatedSubject.tagName]?.map(RelatedSubject.fromTag).singleOrNull,
relatedPubkeys: tags[RelatedPubkey.tagName]?.map(RelatedPubkey.fromTag).toList(),
Expand All @@ -94,10 +90,8 @@ class PrivateDirectMessageData with _$PrivateDirectMessageData, EntityDataWithMe
}

factory PrivateDirectMessageData.fromRawContent(String content) {
final parsedContent = TextParser.allMatchers().parse(content);

return PrivateDirectMessageData(
content: parsedContent,
content: content,
media: {},
uuid: generateUuid(),
);
Expand All @@ -116,14 +110,13 @@ class PrivateDirectMessageData with _$PrivateDirectMessageData, EntityDataWithMe
];

final createdAt = DateTime.now();
final contentString = content.map((match) => match.text).join();

final kind14EventId = EventMessage.calculateEventId(
publicKey: pubkey,
createdAt: createdAt,
kind: PrivateDirectMessageEntity.kind,
tags: eventTags,
content: contentString,
content: content,
);

return EventMessage(
Expand All @@ -132,7 +125,7 @@ class PrivateDirectMessageData with _$PrivateDirectMessageData, EntityDataWithMe
createdAt: createdAt,
kind: PrivateDirectMessageEntity.kind,
tags: eventTags,
content: contentString,
content: content,
sig: null,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,8 @@ class EncryptedGroupRecentChatTile extends HookConsumerWidget {
avatarWidget: groupImageFile != null ? Image.file(groupImageFile) : null,
defaultAvatar: Assets.svg.iconChannelEmptychannel.icon(size: 40.0.s),
lastMessageAt: conversation.latestMessage?.createdAt ?? conversation.joinedAt,
lastMessageContent: entity.content.isEmpty
? context.i18n.empty_message_history
: entity.content.map((e) => e.text).join(),
lastMessageContent:
entity.content.isEmpty ? context.i18n.empty_message_history : entity.content,
unreadMessagesCount: unreadMessagesCount.valueOrNull ?? 0,
onTap: () {
ConversationRoute(conversationId: conversation.conversationId).push<void>(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class TextMessage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entity.data.content.map((e) => e.text).join(),
entity.data.content,
style: context.theme.appTextThemes.body2.copyWith(
color: isMe
? context.theme.appColors.onPrimaryAccent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// SPDX-License-Identifier: ice License 1.0

import 'package:collection/collection.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';
import 'package:ion/app/components/text_editor/utils/build_empty_delta.dart';
import 'package:ion/app/components/text_editor/utils/extract_tags.dart';
import 'package:ion/app/exceptions/exceptions.dart';
import 'package:ion/app/features/core/model/media_type.dart';
import 'package:ion/app/features/core/providers/env_provider.c.dart';
Expand Down Expand Up @@ -32,10 +36,8 @@ import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.da
import 'package:ion/app/features/ion_connect/providers/ion_connect_upload_notifier.c.dart';
import 'package:ion/app/services/compressor/compress_service.c.dart';
import 'package:ion/app/services/logger/logger.dart';
import 'package:ion/app/services/markdown/quill.dart';
import 'package:ion/app/services/media_service/media_service.c.dart';
import 'package:ion/app/services/text_parser/model/text_match.c.dart';
import 'package:ion/app/services/text_parser/model/text_matcher.dart';
import 'package:ion/app/services/text_parser/text_parser.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'create_post_notifier.c.g.dart';
Expand All @@ -46,34 +48,33 @@ class CreatePostNotifier extends _$CreatePostNotifier {
FutureOr<void> build(CreatePostOption createOption) {}

Future<void> create({
required String content,
Delta? content,
WhoCanReplySettingsOption whoCanReply = WhoCanReplySettingsOption.everyone,
EventReference? parentEvent,
EventReference? quotedEvent,
List<MediaFile>? mediaFiles,
String? communtiyId,
String? communityId,
}) async {
state = const AsyncValue.loading();

state = await AsyncValue.guard(() async {
final parsedContent = TextParser.allMatchers().parse(content.trim());

final postContent = content ?? buildEmptyDelta();
final parentEntity = parentEvent != null ? await _getParentEntity(parentEvent) : null;
final (:files, :media) = await _uploadMediaFiles(mediaFiles: mediaFiles);

final postData = ModifiablePostData(
content: _buildContentWithMediaLinks(content: parsedContent, media: media.values.toList()),
content: _buildContentWithMediaLinks(content: postContent, media: media.values.toList()),
media: media,
replaceableEventId: ReplaceableEventIdentifier.generate(),
publishedAt: _buildEntityPublishedAt(),
editingEndedAt: _buildEntityEditingEndedAt(),
relatedHashtags: _buildRelatedHashtags(parsedContent),
relatedHashtags: extractTags(postContent).map((tag) => RelatedHashtag(value: tag)).toList(),
quotedEvent: quotedEvent != null ? _buildQuotedEvent(quotedEvent) : null,
relatedEvents: parentEntity != null ? _buildRelatedEvents(parentEntity) : null,
relatedPubkeys: parentEntity != null ? _buildRelatedPubkeys(parentEntity) : null,
settings: EntityDataWithSettings.build(whoCanReply: whoCanReply),
expiration: _buildExpiration(),
communityId: communtiyId,
communityId: communityId,
);

await _sendPostEntities([...files, postData]);
Expand All @@ -88,14 +89,14 @@ class CreatePostNotifier extends _$CreatePostNotifier {
}

Future<void> modify({
required String content,
required EventReference eventReference,
Delta? content,
WhoCanReplySettingsOption? whoCanReply,
}) async {
state = const AsyncValue.loading();

state = await AsyncValue.guard(() async {
final parsedContent = TextParser.allMatchers().parse(content.trim());
final postContent = content ?? buildEmptyDelta();
final modifiedEntity =
await ref.read(ionConnectEntityProvider(eventReference: eventReference).future);
if (modifiedEntity is! ModifiablePostEntity) {
Expand All @@ -104,10 +105,10 @@ class CreatePostNotifier extends _$CreatePostNotifier {

final postData = modifiedEntity.data.copyWith(
content: _buildContentWithMediaLinks(
content: parsedContent,
content: postContent,
media: modifiedEntity.data.media.values.toList(),
),
relatedHashtags: _buildRelatedHashtags(parsedContent),
relatedHashtags: extractTags(postContent).map((tag) => RelatedHashtag(value: tag)).toList(),
settings: EntityDataWithSettings.build(
whoCanReply: whoCanReply ?? modifiedEntity.data.whoCanReplySetting,
),
Expand All @@ -130,7 +131,7 @@ class CreatePostNotifier extends _$CreatePostNotifier {
}

final postData = entity.data.copyWith(
content: [const TextMatch('')],
content: '',
editingEndedAt: null,
relatedHashtags: [],
relatedPubkeys: [],
Expand Down Expand Up @@ -217,22 +218,18 @@ class CreatePostNotifier extends _$CreatePostNotifier {
};
}

List<TextMatch> _buildContentWithMediaLinks({
required List<TextMatch> content,
String _buildContentWithMediaLinks({
required Delta content,
required List<MediaAttachment> media,
}) {
return [
if (media.isNotEmpty) TextMatch(media.map((attachment) => attachment.url).join(' ')),
if (media.isNotEmpty && content.isNotEmpty) const TextMatch(' '),
...content,
];
}

List<RelatedHashtag> _buildRelatedHashtags(List<TextMatch> content) {
return [
for (final match in content)
if (match.matcher is HashtagMatcher) RelatedHashtag(value: match.text),
];
final contentWithMedia = Delta.fromOperations(
media
.map(
(mediaItem) => Operation.insert(mediaItem.url, {Attribute.link.key: mediaItem.url}),
)
.toList(),
).concat(content);
return deltaToMarkdown(contentWithMedia);
}

List<RelatedEvent> _buildRelatedEvents(IonConnectEntity parentEntity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ion/app/components/text_editor/hooks/use_text_editor_has_content.dart';
Expand Down Expand Up @@ -72,9 +71,6 @@ class PostSubmitButton extends HookConsumerWidget {
return ToolbarSendButton(
enabled: isSubmitButtonEnabled,
onPressed: () async {
final operations = textEditorController.document.toDelta().operations;
final content = Document.fromDelta(Delta.fromOperations(operations)).toPlainText();

if (modifiedEvent != null) {
unawaited(
ref
Expand All @@ -84,7 +80,7 @@ class PostSubmitButton extends HookConsumerWidget {
).notifier,
)
.modify(
content: content,
content: textEditorController.document.toDelta(),
eventReference: modifiedEvent!,
whoCanReply: whoCanReply,
),
Expand All @@ -101,7 +97,7 @@ class PostSubmitButton extends HookConsumerWidget {
).notifier,
)
.create(
content: content,
content: textEditorController.document.toDelta(),
parentEvent: parentEvent,
quotedEvent: quotedEvent,
mediaFiles: convertedMediaFiles,
Expand Down
Loading

0 comments on commit 6f43f1d

Please sign in to comment.