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

TW-1774: Update avatar when have changes #1953

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
9 changes: 7 additions & 2 deletions lib/pages/chat_list/chat_list_body_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ class ChatListBodyView extends StatelessWidget {
),
stream: controller.activeClient.onSync.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, _) {
.rateLimitWithSyncUpdate(const Duration(seconds: 1)),
builder: (context, syncUpdateSnapshot) {
Logs().v(
'ChatListBodyView: StreamBuilder: snapshot: ${syncUpdateSnapshot.data?.rooms}',
);
if (controller.activeFilter == ActiveFilter.spaces) {
return SpaceView(
controller,
Expand Down Expand Up @@ -163,6 +166,7 @@ class ChatListBodyView extends StatelessWidget {
child: ChatListViewBuilder(
controller: controller,
rooms: controller.filteredRoomsForPin,
syncUpdate: syncUpdateSnapshot.data,
),
),
if (!controller.filteredRoomsForAllIsEmpty)
Expand Down Expand Up @@ -192,6 +196,7 @@ class ChatListBodyView extends StatelessWidget {
child: ChatListViewBuilder(
controller: controller,
rooms: controller.filteredRoomsForAll,
syncUpdate: syncUpdateSnapshot.data,
),
),
],
Expand Down
14 changes: 6 additions & 8 deletions lib/pages/chat_list/chat_list_item.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item_avatar.dart';
import 'package:fluffychat/presentation/mixins/chat_list_item_mixin.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item_style.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item_subtitle.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item_title.dart';
import 'package:fluffychat/utils/dialog/twake_dialog.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/twake_snackbar.dart';
import 'package:fluffychat/widgets/avatar/avatar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

Expand All @@ -27,6 +26,7 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin {
final void Function()? onTapAvatar;
final void Function(TapDownDetails)? onSecondaryTapDown;
final void Function()? onLongPress;
final JoinedRoomUpdate? joinedRoomUpdate;

const ChatListItem(
this.room, {
Expand All @@ -39,6 +39,7 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin {
this.onSecondaryTapDown,
this.onLongPress,
super.key,
this.joinedRoomUpdate,
});

void clickAction(BuildContext context) async {
Expand Down Expand Up @@ -87,9 +88,6 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin {

@override
Widget build(BuildContext context) {
final displayName = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
return Padding(
padding: ChatListItemStyle.paddingConversation,
child: Material(
Expand All @@ -114,10 +112,10 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin {
padding: ChatListItemStyle.paddingAvatar,
child: Stack(
children: [
Avatar(
mxContent: room.avatar,
name: displayName,
ChatListItemAvatar(
room: room,
onTap: onTapAvatar,
joinedRoomUpdate: joinedRoomUpdate,
),
if (_isGroupChat)
Positioned(
Expand Down
104 changes: 104 additions & 0 deletions lib/pages/chat_list/chat_list_item_avatar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar/avatar.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

class ChatListItemAvatar extends StatefulWidget {
final Room room;
final void Function()? onTap;
final JoinedRoomUpdate? joinedRoomUpdate;

const ChatListItemAvatar({
required this.room,
this.onTap,
this.joinedRoomUpdate,
super.key,
});

@override
State<ChatListItemAvatar> createState() => _ChatListItemAvatarState();
}

class _ChatListItemAvatarState extends State<ChatListItemAvatar> {
final ValueNotifier<Uri?> avatarUrlNotifier = ValueNotifier<Uri>(Uri());

@override
void initState() {
avatarUrlNotifier.value = widget.room.avatar ?? Uri();
super.initState();
}

@override
void dispose() {
avatarUrlNotifier.dispose();
super.dispose();
}

@override
void didUpdateWidget(ChatListItemAvatar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.joinedRoomUpdate != widget.joinedRoomUpdate) {
updateAvatarUrlFromJoinedRoomUpdate();
}
}

@override
Widget build(BuildContext context) {
final displayName = widget.room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
return ValueListenableBuilder(
valueListenable: avatarUrlNotifier,
builder: (context, avatarUrl, child) {
return Avatar(
mxContent: avatarUrl,
name: displayName,
onTap: widget.onTap,
);
},
);
}

void updateAvatarUrlFromJoinedRoomUpdate() {
if (isChatHaveAvatarUpdated) {
if (isGroupChatAvatarUpdated) {
updateGroupAvatar();
} else if (isDirectChatAvatarUpdated) {
updateDirectChatAvatar();
}
}
}

bool get isChatHaveAvatarUpdated =>
widget.joinedRoomUpdate?.timeline?.events?.isNotEmpty == true;

bool get isDirectChatAvatarUpdated {
return widget.room.isDirectChat &&
widget.joinedRoomUpdate?.timeline?.events?.last.type ==
EventTypes.RoomMember;
}

bool get isGroupChatAvatarUpdated =>
widget.joinedRoomUpdate?.timeline?.events?.last.type ==
EventTypes.RoomAvatar;

void updateDirectChatAvatar() {
final event = widget.joinedRoomUpdate?.timeline?.events?.last;
final avatarMxc = event?.content['avatar_url'];
if (avatarMxc is String &&
avatarMxc.isNotEmpty &&
event?.senderId == widget.room.directChatMatrixID) {
avatarUrlNotifier.value = Uri.tryParse(avatarMxc);
}
}

void updateGroupAvatar() {
final avatarMxc =
widget.joinedRoomUpdate?.timeline?.events?.last.content['url'];

if (avatarMxc is String && avatarMxc.isNotEmpty) {
avatarUrlNotifier.value = Uri.tryParse(avatarMxc);
}
}
}
4 changes: 4 additions & 0 deletions lib/pages/chat_list/chat_list_view_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import 'package:matrix/matrix.dart';
class ChatListViewBuilder extends StatelessWidget {
final ChatListController controller;
final List<Room> rooms;
final SyncUpdate? syncUpdate;

const ChatListViewBuilder({
super.key,
required this.controller,
required this.rooms,
this.syncUpdate,
});

@override
Expand All @@ -36,12 +38,14 @@ class ChatListViewBuilder extends StatelessWidget {
chatListItem: CommonChatListItem(
controller: controller,
room: rooms[index],
joinedRoomUpdate: syncUpdate?.rooms?.join?[rooms[index].id],
),
);
}
return CommonChatListItem(
controller: controller,
room: rooms[index],
joinedRoomUpdate: syncUpdate?.rooms?.join?[rooms[index].id],
);
},
);
Expand Down
3 changes: 3 additions & 0 deletions lib/pages/chat_list/common_chat_list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import 'package:matrix/matrix.dart';
class CommonChatListItem extends StatelessWidget {
final ChatListController controller;
final Room room;
final JoinedRoomUpdate? joinedRoomUpdate;

const CommonChatListItem({
super.key,
required this.controller,
required this.room,
this.joinedRoomUpdate,
});

@override
Expand Down Expand Up @@ -49,6 +51,7 @@ class CommonChatListItem extends StatelessWidget {
},
),
activeChat: activeRoomId == room.id,
joinedRoomUpdate: joinedRoomUpdate,
);
},
);
Expand Down
42 changes: 42 additions & 0 deletions lib/utils/stream_extension.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:async';

import 'package:matrix/matrix.dart';

extension StreamExtension on Stream {
/// Returns a new Stream which outputs only `true` for every update of the original
/// stream, ratelimited by the Duration t
Expand Down Expand Up @@ -45,4 +47,44 @@ extension StreamExtension on Stream {
};
return controller.stream;
}

Stream<SyncUpdate> rateLimitWithSyncUpdate(Duration t) {
Te-Z marked this conversation as resolved.
Show resolved Hide resolved
final controller = StreamController<SyncUpdate>();
Timer? timer;
SyncUpdate? pendingMessage;

void processMessage() {
if (controller.isClosed) return;

if (timer == null) {
Te-Z marked this conversation as resolved.
Show resolved Hide resolved
if (pendingMessage != null) {
controller.add(pendingMessage!);
pendingMessage = null;
timer = Timer(t, () {
timer = null;
if (pendingMessage != null) {
processMessage();
}
});
}
}
}

final subscription = listen(
(data) {
pendingMessage = data;
processMessage();
},
onDone: () => controller.close(),
onError: (e, s) => controller.addError(e, s),
);

controller.onCancel = () {
subscription.cancel();
timer?.cancel();
controller.close();
};

return controller.stream;
}
}
12 changes: 12 additions & 0 deletions test/utils/mock_sync_update.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:matrix/matrix.dart';
import 'package:mockito/mockito.dart';

class MockSyncUpdate extends Mock implements SyncUpdate {
@override
final String nextBatch;

MockSyncUpdate({required this.nextBatch});

@override
String toString() => 'MockSyncUpdate(nextBatch: $nextBatch)';
}
Loading