feat: group admin edit, media tab, confirm modals#2490
Conversation
| File? groupImageFile; | ||
| if (groupImageFileResult.hasData) { | ||
| final file = groupImageFileResult.data; | ||
| if (file != null && file.existsSync() && file.lengthSync() > 0) { | ||
| groupImageFile = file; | ||
| } | ||
| } |
There was a problem hiding this comment.
for each rebuild, it ll check length and exists sync, should we keep in state and perform only when related file is changed?
| // Extract links from content | ||
| final content = entity.data.content; | ||
| if (content.isNotEmpty) { | ||
| final textParser = TextParser(matchers: {const UrlMatcher()}); |
There was a problem hiding this comment.
we can define it out of the for loop to do not create for each item. wdyt?
| for (final messages in messagesByDate.values) { | ||
| for (final eventMessage in messages) { | ||
| try { | ||
| final entity = EncryptedGroupMessageEntity.fromEventMessage(eventMessage); | ||
| final eventReference = entity.toEventReference(); | ||
| final publishedAt = entity.data.publishedAt.value; | ||
|
|
||
| // Extract links from content | ||
| final content = entity.data.content; | ||
| if (content.isNotEmpty) { | ||
| final textParser = TextParser(matchers: {const UrlMatcher()}); | ||
| final parsed = textParser.parse(content, onlyMatches: true); | ||
| final links = parsed | ||
| .where((match) => match.matcher is UrlMatcher) | ||
| .map((match) => match.text) | ||
| .toSet(); // Use Set to avoid duplicates | ||
|
|
||
| for (final link in links) { | ||
| // Exclude media URLs (they're already in media attachments) | ||
| final isMediaUrl = entity.data.media.values.any( | ||
| (media) => media.url == link || media.thumb == link || media.image == link, | ||
| ); | ||
| if (!isMediaUrl) { | ||
| allLinks.add( | ||
| GroupLinkItem( | ||
| url: link, | ||
| eventReference: eventReference, | ||
| publishedAt: publishedAt, | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Let’s assume a group conversation has a few thousand messages. I believe it’s quite feasible to reach that count for groups. Does this cause UI lag? We could load it in pages to enhance performance. What are your thoughts on this? If it does cause lag, I’m also open to handling follow-up tasks.
There was a problem hiding this comment.
same concerns for files, media and voice tabs
| itemBuilder: (context, int index) { | ||
| final fileItem = files[index]; | ||
| return _GroupFileCell( | ||
| fileName: fileItem.media.alt ?? 'File', |
There was a problem hiding this comment.
all files should have alt prop which indicates the file name - if we need it hence of null property definition, we can define this magic string into the l18n. wdyt?
| ) | ||
| : null; | ||
|
|
||
| final fileSizeStr = localMediaPath != null ? formattedFileSize(localMediaPath) ?? '0kb' : '0kb'; |
There was a problem hiding this comment.
are we sure about showing 0kb while loading the media? we can align it with designer wdyt?
| String _formatDateAndTime(int timestamp) { | ||
| final date = timestamp.toDateTime; | ||
| final dateStr = DateFormat('d MMM yyyy').format(date); | ||
| final timeStr = DateFormat('HH:mm').format(date); | ||
| return '$dateStr • $timeStr'; | ||
| } |
There was a problem hiding this comment.
can we handle in one DateFormat('d MMM yyyy • HH:mm');
also we can move it to the date.dart util class. wdyt?
| part 'group_links_provider.r.g.dart'; | ||
|
|
||
| @riverpod | ||
| Future<List<GroupLinkItem>> groupLinks( |
There was a problem hiding this comment.
Consider creating GroupLinksService class for the logic and use Riverpod only to create it's instance, like this:
@riverpod
GroupLinksService groupLinksService(GroupLinksServiceRef ref) {
final dao = ref.watch(conversationMessageDaoProvider);
final parser = TextParser(matchers: {const UrlMatcher()});
return GroupLinksService(
conversationMessageDao: dao,
textParser: parser,
);
}
| part 'group_media_provider.r.g.dart'; | ||
|
|
||
| @riverpod | ||
| Future<List<GroupMediaItem>> groupMedia( |
There was a problem hiding this comment.
Consider extracting all parsing, flattening, and sorting work into a dedicated GroupMediaService class.
Use Riverpod only to construct the service and call its methods:
@riverpod
GroupMediaService groupMediaService(GroupMediaServiceRef ref) {
final dao = ref.watch(conversationMessageDaoProvider);
return GroupMediaService(dao);
}
Then groupMedia, groupMediaItems, groupVoiceItems, and groupFilesItems should only delegate to this service instead of performing business logic inside the providers.
| final userPreviewData = ref.watch(userPreviewDataProvider(participantMasterkey)).valueOrNull; | ||
|
|
||
| if (userPreviewData == null) { | ||
| return const SizedBox.shrink(); |
There was a problem hiding this comment.
Do we need a loader here?
| Widget build(BuildContext context, WidgetRef ref) { | ||
| final groupMetadata = ref.watch(encryptedGroupMetadataProvider(conversationId)).valueOrNull; | ||
|
|
||
| final adminsAndOwners = groupMetadata?.members |
There was a problem hiding this comment.
This is a business logic, consider extracting it from the widget.
There was a problem hiding this comment.
Creating sortedAdmins too.
There was a problem hiding this comment.
Also calculating isMaxAdminsReached.
| .toList(); | ||
|
|
||
| if (adminsAndOwners == null || adminsAndOwners.isEmpty) { | ||
| return const SizedBox.shrink(); |
There was a problem hiding this comment.
Should a skeleton/loading be implemented here?
| ), | ||
| ).data | ||
| : null; | ||
| final mediaFuture = useMemoized<Future<File?>>( |
There was a problem hiding this comment.
This logic belongs in a provider, not in a widget hook.
|
|
||
| @override | ||
| Widget build(BuildContext context, WidgetRef ref) { | ||
| final eventMessageFuture = useFuture( |
There was a problem hiding this comment.
This logic belongs in a provider, not in a widget hook.
| ); | ||
| } | ||
|
|
||
| final fileFuture = useFuture<String?>( |
There was a problem hiding this comment.
This logic belongs in a provider, not in a widget hook.
| .select(userPreviewDisplayNameSelector), | ||
| ); | ||
|
|
||
| final eventMessageFuture = useFuture( |
There was a problem hiding this comment.
This logic belongs in a provider, not in a widget hook.
| ), | ||
| ); | ||
|
|
||
| final entity = useMemoized( |
There was a problem hiding this comment.
This logic belongs in a provider, not in a widget hook.
| final audioPlaybackController = useAudioWavePlaybackController() | ||
| ..setFinishMode(finishMode: FinishMode.pause); | ||
|
|
||
| final eventMessageId = useMemoized( |
There was a problem hiding this comment.
This logic belongs in a provider, not in a widget hook.
| label: Text(context.i18n.button_delete), | ||
| onPressed: () { | ||
| context.pop(); | ||
| //todo add delete conversation logic |
There was a problem hiding this comment.
let's use standard formatting for TODOs, so that they would be shown in the IDE, e.g. // TODO(ice-erebus): add recent coins
internal review
group edit page
group admin media tab
confirmation modals