diff --git a/lib/service/database/migrations/0004_groupchat_members.dart b/lib/service/database/migrations/0004_groupchat_members.dart index 054e82f1..d88a6ccc 100644 --- a/lib/service/database/migrations/0004_groupchat_members.dart +++ b/lib/service/database/migrations/0004_groupchat_members.dart @@ -15,6 +15,7 @@ Future upgradeFromV48ToV49(DatabaseMigrationData data) async { avatarPath TEXT, avatarHash TEXT, realJid TEXT, + isSelf INTEGER NOT NULL, PRIMARY KEY (roomJid, accountJid, nick), CONSTRAINT fk_muc FOREIGN KEY (roomJid, accountJid) diff --git a/lib/service/groupchat.dart b/lib/service/groupchat.dart index a259d5a7..05bbe29c 100644 --- a/lib/service/groupchat.dart +++ b/lib/service/groupchat.dart @@ -84,6 +84,7 @@ class GroupchatService { null, null, null, + false, ); await db.insert( groupchatMembersTable, @@ -91,7 +92,23 @@ class GroupchatService { ); members.add(member); } - members.sort((a, b) => a.nick.compareTo(b.nick)); + // Add the self-participant + await db.insert( + groupchatMembersTable, + GroupchatMember( + accountJid, + muc.toString(), + state.nick!, + state.role!, + state.affiliation!, + null, + null, + null, + true, + ).toJson(), + ); + + // TODO(Unknown): In case the MUC changed our nick, update the groupchat details to reflect this. return Result( GroupchatDetails( diff --git a/lib/shared/models/groupchat_member.dart b/lib/shared/models/groupchat_member.dart index a5974656..6f404ec4 100644 --- a/lib/shared/models/groupchat_member.dart +++ b/lib/shared/models/groupchat_member.dart @@ -1,9 +1,25 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxyv2/service/database/helpers.dart'; part 'groupchat_member.freezed.dart'; part 'groupchat_member.g.dart'; +// TODO: Move somewhere else so that we can reuse it. +class BooleanTypeConverter extends JsonConverter { + const BooleanTypeConverter(); + + @override + bool fromJson(int json) { + return intToBool(json); + } + + @override + int toJson(bool object) { + return boolToInt(object); + } +} + class RoleTypeConverter extends JsonConverter { const RoleTypeConverter(); @@ -43,6 +59,7 @@ class GroupchatMember with _$GroupchatMember { String? avatarPath, String? avatarHash, String? realJid, + @BooleanTypeConverter() bool isSelf, ) = _GroupchatMember; /// JSON diff --git a/lib/ui/pages/profile/profile_view.dart b/lib/ui/pages/profile/profile_view.dart index 43927100..e60f6be4 100644 --- a/lib/ui/pages/profile/profile_view.dart +++ b/lib/ui/pages/profile/profile_view.dart @@ -8,6 +8,7 @@ import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/models/conversation.dart'; import 'package:moxxyv2/shared/models/groupchat_member.dart'; import 'package:moxxyv2/ui/bloc/server_info_bloc.dart'; +import 'package:moxxyv2/ui/constants.dart'; import 'package:moxxyv2/ui/pages/profile/conversationheader.dart'; import 'package:moxxyv2/ui/pages/profile/profile.dart'; import 'package:moxxyv2/ui/pages/profile/selfheader.dart'; @@ -55,24 +56,9 @@ class ProfileViewState extends State { ), ))! as GroupchatMembersResult; - // TODO: Handle the display of our own data more gracefully. Maybe keep a special - // GroupchatMember that also stores our own affiliation and role so that we can - // cache it. // TODO: That also requires that we render that element separately so that we can just bypass // the avatar data and just pull it from one of the BLoCs. final members = List.of(result.members) - ..add( - GroupchatMember( - '', - '', - t.messages.you, - Role.none, - Affiliation.none, - null, - null, - null, - ), - ) ..sort(groupchatMemberSortingFunction); setState(() { @@ -86,6 +72,55 @@ class ProfileViewState extends State { _initStateAsync(); } + Widget _buildMemberTile(GroupchatMember member) { + if (member.isSelf) { + return ListTile( + leading: CachingXMPPAvatar.self(radius: 20), + title: Text( + t.messages.you, + style: const TextStyle( + color: primaryColor, + ), + ), + subtitle: Text( + member.nick, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + return ListTile( + leading: CachingXMPPAvatar( + jid: '${widget.arguments.jid}/${member.nick}', + radius: 20, + hasContactId: false, + isGroupchat: true, + // TODO(Unknown): Request avatars at some point + shouldRequest: false, + ), + title: Text( + member.nick, + overflow: TextOverflow.ellipsis, + ), + subtitle: switch (member.affiliation) { + // TODO: i18n + Affiliation.owner => const Text( + 'Owner', + style: TextStyle(color: Colors.red), + overflow: TextOverflow.ellipsis, + ), + Affiliation.admin => const Text( + 'Admin', + style: TextStyle(color: Colors.green), + overflow: TextOverflow.ellipsis, + ), + Affiliation.member => null, + Affiliation.none => null, + Affiliation.outcast => null, + }, + ); + } + } + Widget _buildMemberList() { if (_members == null) { return const SliverToBoxAdapter( @@ -96,30 +131,8 @@ class ProfileViewState extends State { return SliverList.builder( itemCount: _members!.length, itemBuilder: (context, index) { - return ListTile( - leading: CachingXMPPAvatar( - jid: '${widget.arguments.jid}/${_members![index].nick}', - radius: 20, - hasContactId: false, - isGroupchat: true, - shouldRequest: false, - ), - title: Text(_members![index].nick), - subtitle: switch (_members![index].affiliation) { - // TODO: i18n - Affiliation.owner => const Text( - 'Owner', - style: TextStyle(color: Colors.red), - ), - Affiliation.admin => const Text( - 'Admin', - style: TextStyle(color: Colors.green), - ), - Affiliation.member => null, - Affiliation.none => null, - Affiliation.outcast => null, - }, - ); + final member = _members![index]; + return _buildMemberTile(member); }, ); }