Skip to content

Conversation

@ice-priapus
Copy link
Contributor

internal review

  • group edit page

  • group admin media tab

  • confirmation modals

Comment on lines +45 to +51
File? groupImageFile;
if (groupImageFileResult.hasData) {
final file = groupImageFileResult.data;
if (file != null && file.existsSync() && file.lengthSync() > 0) {
groupImageFile = file;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can define it out of the for loop to do not create for each item. wdyt?

Comment on lines +25 to +57
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,
),
);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same concerns for files, media and voice tabs

itemBuilder: (context, int index) {
final fileItem = files[index];
return _GroupFileCell(
fileName: fileItem.media.alt ?? 'File',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure about showing 0kb while loading the media? we can align it with designer wdyt?

Comment on lines +87 to +92
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';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a loader here?

Widget build(BuildContext context, WidgetRef ref) {
final groupMetadata = ref.watch(encryptedGroupMetadataProvider(conversationId)).valueOrNull;

final adminsAndOwners = groupMetadata?.members
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a business logic, consider extracting it from the widget.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating sortedAdmins too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also calculating isMaxAdminsReached.

.toList();

if (adminsAndOwners == null || adminsAndOwners.isEmpty) {
return const SizedBox.shrink();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a skeleton/loading be implemented here?

),
).data
: null;
final mediaFuture = useMemoized<Future<File?>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic belongs in a provider, not in a widget hook.


@override
Widget build(BuildContext context, WidgetRef ref) {
final eventMessageFuture = useFuture(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic belongs in a provider, not in a widget hook.

);
}

final fileFuture = useFuture<String?>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic belongs in a provider, not in a widget hook.

.select(userPreviewDisplayNameSelector),
);

final eventMessageFuture = useFuture(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic belongs in a provider, not in a widget hook.

),
);

final entity = useMemoized(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic belongs in a provider, not in a widget hook.

final audioPlaybackController = useAudioWavePlaybackController()
..setFinishMode(finishMode: FinishMode.pause);

final eventMessageId = useMemoized(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use standard formatting for TODOs, so that they would be shown in the IDE, e.g. // TODO(ice-erebus): add recent coins

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants