Compare commits

...

3 Commits

Author SHA1 Message Date
018386d14e 💄 Optimize chat message cursor 2025-10-08 22:42:43 +08:00
3825d7c6c7 💄 Optimize display of certain type of message item 2025-10-08 22:33:56 +08:00
bf930291e4 ♻️ Update the embed rendering 2025-10-08 21:58:24 +08:00
5 changed files with 153 additions and 141 deletions

View File

@@ -312,6 +312,8 @@
"settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display",
"messageUpdateLinks": "Server generated links previews",
"messageUpdateEdited": "Edited a message",
"unreadMessages": {
"one": "{} unread message",
"other": "{} unread messages"
@@ -1199,5 +1201,6 @@
"addRemarkForTransfer": "Add remark for transfer",
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
"transferCreatedSuccessfully": "Transfer created successfully!",
"postUpdate": "Update"
"postUpdate": "Update",
"fileMetadata": "File Metadata"
}

View File

@@ -59,18 +59,24 @@ class MessageContent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Symbols.edit,
item.type == 'messages.update.links'
? Symbols.link
: Symbols.edit,
size: 16,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
).padding(top: 2),
),
const Gap(4),
if (item.meta['previous_content'] is String)
Flexible(
child: PrettyDiffText(
oldText: item.meta['previous_content'],
newText: item.content ?? 'Edited a message',
newText:
item.content ??
(item.type == 'messages.update.links'
? 'messageUpdateLinks'.tr()
: 'messageUpdateEdited'.tr()),
defaultTextStyle: Theme.of(
context,
).textTheme.bodyMedium!.copyWith(
@@ -107,10 +113,13 @@ class MessageContent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: MarkdownTextContent(
content: item.content ?? '*${item.type} has no content*',
isSelectable: true,
linesMargin: EdgeInsets.zero,
child: MouseRegion(
cursor: SystemMouseCursors.text,
child: MarkdownTextContent(
content: item.content ?? '*${item.type} has no content*',
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
),
),
if (translatedText?.isNotEmpty ?? false)
@@ -131,10 +140,13 @@ class MessageContent extends StatelessWidget {
],
).padding(vertical: 4),
),
MarkdownTextContent(
content: translatedText!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
MouseRegion(
cursor: SystemMouseCursors.text,
child: MarkdownTextContent(
content: translatedText!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
),
]),
],

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
@@ -10,12 +9,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart';
import 'package:island/models/embed.dart';
import 'package:island/pods/chat/chat_rooms.dart';
import 'package:island/pods/chat/messages_notifier.dart';
import 'package:island/pods/translate.dart';
import 'package:island/pods/config.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/message_content.dart';
@@ -24,7 +21,8 @@ import 'package:island/widgets/chat/message_sender_info.dart';
import 'package:island/widgets/content/alert.native.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:island/widgets/content/embed/embed_list.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/content/sheet.dart';
@@ -163,6 +161,8 @@ class MessageItem extends HookConsumerWidget {
: Colors.transparent;
return InkWell(
mouseCursor: MouseCursor.defer,
focusColor: Colors.transparent,
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
onTap: () {
@@ -420,28 +420,16 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (remoteMessage.meta['embeds'] != null &&
kMessageEnableEmbedTypes.contains(message.type))
EmbedListWidget(
embeds:
remoteMessage.meta['embeds'] as List<dynamic>,
isInteractive: true,
isFullPost: false,
renderingPadding: EdgeInsets.zero,
maxWidth: 480,
),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -591,23 +579,15 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(constraints.maxWidth, 480),
margin: const EdgeInsets.symmetric(vertical: 4),
);
},
),
)
.toList()),
if (remoteMessage.meta['embeds'] != null &&
kMessageEnableEmbedTypes.contains(message.type))
EmbedListWidget(
embeds: remoteMessage.meta['embeds'] as List<dynamic>,
isInteractive: true,
isFullPost: false,
renderingPadding: EdgeInsets.zero,
maxWidth: 480,
),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -737,28 +717,15 @@ class MessageItemDisplayDiscord extends HookConsumerWidget {
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (remoteMessage.meta['embeds'] != null &&
kMessageEnableEmbedTypes.contains(message.type))
EmbedListWidget(
embeds: remoteMessage.meta['embeds'] as List<dynamic>,
isInteractive: true,
isFullPost: false,
renderingPadding: EdgeInsets.zero,
maxWidth: 480,
),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -841,25 +808,15 @@ class MessageItemDisplayDiscord extends HookConsumerWidget {
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(constraints.maxWidth, 480),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (remoteMessage.meta['embeds'] != null &&
kMessageEnableEmbedTypes.contains(message.type))
EmbedListWidget(
embeds: remoteMessage.meta['embeds'] as List<dynamic>,
isInteractive: true,
isFullPost: false,
renderingPadding: EdgeInsets.zero,
maxWidth: 480,
),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@@ -0,0 +1,76 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:island/models/embed.dart';
import 'package:island/models/poll.dart';
import 'package:island/services/responsive.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:island/widgets/poll/poll_submit.dart';
import 'package:styled_widget/styled_widget.dart';
class EmbedListWidget extends StatelessWidget {
final List<dynamic> embeds;
final bool isInteractive;
final bool isFullPost;
final EdgeInsets renderingPadding;
final double? maxWidth;
const EmbedListWidget({
super.key,
required this.embeds,
this.isInteractive = true,
this.isFullPost = false,
this.renderingPadding = EdgeInsets.zero,
this.maxWidth,
});
@override
Widget build(BuildContext context) {
return Column(
children:
embeds
.map((embedData) => convertMapKeysToSnakeCase(embedData))
.map(
(embedData) => switch (embedData['type']) {
'link' => EmbedLinkWidget(
link: SnScrappedLink.fromJson(embedData),
maxWidth:
maxWidth ??
math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
),
margin: EdgeInsets.only(
top: 4,
bottom: 4,
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
),
'poll' => Card(
margin: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 8,
),
child:
embedData['poll'] == null
? const Text('Poll was not loaded...')
: PollSubmit(
initialAnswers:
embedData['poll']?['user_answer']?['answer'],
stats: embedData['poll']?['stats'],
poll: SnPollWithStats.fromJson(embedData['poll']),
onSubmit: (_) {},
isReadonly: !isInteractive,
isInitiallyExpanded: isFullPost,
).padding(horizontal: 16, vertical: 12),
),
_ => Text('Unable show embed: ${embedData['type']}'),
},
)
.toList(),
);
}
}

View File

@@ -1,24 +1,18 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/embed.dart';
import 'package:island/models/poll.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
import 'package:island/widgets/content/embed/embed_list.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/poll/poll_submit.dart';
import 'package:island/widgets/post/post_replies_sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -26,6 +20,8 @@ import 'package:styled_widget/styled_widget.dart';
part 'post_shared.g.dart';
const kMessageEnableEmbedTypes = ['text', 'messages.new'];
@riverpod
Future<SnPost?> postFeaturedReply(Ref ref, String id) async {
final client = ref.watch(apiClientProvider);
@@ -874,44 +870,12 @@ class PostBody extends ConsumerWidget {
],
).padding(horizontal: renderingPadding.horizontal + 4, top: 4),
if (item.meta?['embeds'] != null)
...((item.meta!['embeds'] as List<dynamic>)
.map((embedData) => convertMapKeysToSnakeCase(embedData))
.map(
(embedData) => switch (embedData['type']) {
'link' => EmbedLinkWidget(
link: SnScrappedLink.fromJson(embedData),
maxWidth: math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
),
margin: EdgeInsets.only(
top: 4,
bottom: 4,
left: renderingPadding.horizontal,
right: renderingPadding.horizontal,
),
),
'poll' => Card(
margin: EdgeInsets.symmetric(
horizontal: renderingPadding.horizontal,
vertical: 8,
),
child:
embedData['poll'] == null
? const Text('Poll was not loaded...')
: PollSubmit(
initialAnswers:
embedData['poll']?['user_answer']?['answer'],
stats: embedData['poll']?['stats'],
poll: SnPollWithStats.fromJson(embedData['poll']),
onSubmit: (_) {},
isReadonly: !isInteractive,
isInitiallyExpanded: isFullPost,
).padding(horizontal: 16, vertical: 12),
),
_ => Text('Unable show embed: ${embedData['type']}'),
},
)),
EmbedListWidget(
embeds: item.meta!['embeds'] as List<dynamic>,
isInteractive: isInteractive,
isFullPost: isFullPost,
renderingPadding: renderingPadding,
),
],
);
}