Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: post markdown content #692

Merged
merged 11 commits into from
Feb 12, 2025
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');
}
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 @@ -35,7 +36,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