feat(ui,service): Get a basic participant list going

This commit is contained in:
2023-09-29 21:35:56 +02:00
parent 3f9ae41f2c
commit a62a4e0da1
12 changed files with 320 additions and 47 deletions

View File

@@ -355,6 +355,14 @@ files:
items: items:
type: List<SendFilesRecipient> type: List<SendFilesRecipient>
deserialise: true deserialise: true
- name: GroupchatMembersResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
members:
type: List<GroupchatMember>
deserialise: true
generate_builder: true generate_builder: true
builder_name: "Event" builder_name: "Event"
builder_baseclass: "BackgroundEvent" builder_baseclass: "BackgroundEvent"
@@ -714,6 +722,12 @@ files:
- JsonImplementation - JsonImplementation
attributes: attributes:
jids: List<String> jids: List<String>
- name: GetMembersForGroupchatCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
generate_builder: true generate_builder: true
# get${builder_Name}FromJson # get${builder_Name}FromJson
builder_name: "Command" builder_name: "Command"

View File

@@ -18,6 +18,7 @@ const omemoRatchetsTable = 'OmemoRatchets';
const omemoTrustTable = 'OmemoTrustTable'; const omemoTrustTable = 'OmemoTrustTable';
const notificationsTable = 'Notifications'; const notificationsTable = 'Notifications';
const groupchatTable = 'Groupchat'; const groupchatTable = 'Groupchat';
const groupchatMembersTable = 'GroupchatMembers';
const typeString = 0; const typeString = 0;
const typeInt = 1; const typeInt = 1;

View File

@@ -50,6 +50,7 @@ import 'package:moxxyv2/service/database/migrations/0003_notifications.dart';
import 'package:moxxyv2/service/database/migrations/0003_occupant_id.dart'; import 'package:moxxyv2/service/database/migrations/0003_occupant_id.dart';
import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart'; import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
import 'package:moxxyv2/service/database/migrations/0003_sticker_pack_timestamp.dart'; import 'package:moxxyv2/service/database/migrations/0003_sticker_pack_timestamp.dart';
import 'package:moxxyv2/service/database/migrations/0004_groupchat_members.dart';
import 'package:moxxyv2/service/database/migrations/0004_new_avatar_cache.dart'; import 'package:moxxyv2/service/database/migrations/0004_new_avatar_cache.dart';
import 'package:moxxyv2/service/xmpp_state.dart'; import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@@ -109,6 +110,7 @@ const List<Migration<DatabaseMigrationData>> migrations = [
Migration(46, upgradeFromV45ToV46), Migration(46, upgradeFromV45ToV46),
Migration(47, upgradeFromV46ToV47), Migration(47, upgradeFromV46ToV47),
Migration(48, upgradeFromV47ToV48), Migration(48, upgradeFromV47ToV48),
Migration(49, upgradeFromV48ToV49),
]; ];
class DatabaseService { class DatabaseService {

View File

@@ -0,0 +1,28 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV48ToV49(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'''
CREATE TABLE $groupchatMembersTable (
roomJid TEXT NOT NULL,
accountJid TEXT NOT NULL,
nick TEXT NOT NULL,
role TEXT NOT NULL,
affiliation TEXT NOT NULL,
avatarPath TEXT,
avatarHash TEXT,
realJid TEXT,
PRIMARY KEY (roomJid, accountJid, nick),
CONSTRAINT fk_muc
FOREIGN KEY (roomJid, accountJid)
REFERENCES $conversationsTable (jid, accountJid)
ON DELETE CASCADE
)''',
);
await db.execute(
'CREATE INDEX idx_members ON $groupchatMembersTable (roomJid, accountJid)',
);
}

View File

@@ -118,6 +118,7 @@ void setupBackgroundEventHandler() {
performFetchRecipientInformation, performFetchRecipientInformation,
), ),
EventTypeMatcher<ExitConversationCommand>(performConversationExited), EventTypeMatcher<ExitConversationCommand>(performConversationExited),
EventTypeMatcher<GetMembersForGroupchatCommand>(performGetMembers),
]); ]);
GetIt.I.registerSingleton<EventHandler>(handler); GetIt.I.registerSingleton<EventHandler>(handler);
@@ -336,13 +337,10 @@ Future<void> performAddConversation(
); );
if (conversation!.type == ConversationType.groupchat) { if (conversation!.type == ConversationType.groupchat) {
await GetIt.I await GetIt.I.get<GroupchatService>().joinRoom(
.get<XmppConnection>()
.getManagerById<MUCManager>(mucManager)!
.joinRoom(
JID.fromString(conversation.jid), JID.fromString(conversation.jid),
accountJid,
conversation.groupchatDetails!.nick, conversation.groupchatDetails!.nick,
maxHistoryStanzas: 0,
); );
} }
} }
@@ -1631,18 +1629,6 @@ Future<void> performJoinGroupchat(
); );
} else { } else {
// We did not have a conversation with that JID. // We did not have a conversation with that JID.
final joinRoomResult = await GetIt.I.get<GroupchatService>().joinRoom(
JID.fromString(jid),
accountJid,
nick,
);
if (joinRoomResult.isType<GroupchatErrorType>()) {
sendEvent(
ErrorEvent(errorId: joinRoomResult.get<GroupchatErrorType>().value),
id: id,
);
}
await cs.createOrUpdateConversation( await cs.createOrUpdateConversation(
jid, jid,
accountJid, accountJid,
@@ -1682,6 +1668,19 @@ Future<void> performJoinGroupchat(
return newConversation; return newConversation;
}, },
); );
// Join the room.
final joinRoomResult = await GetIt.I.get<GroupchatService>().joinRoom(
JID.fromString(jid),
accountJid,
nick,
);
if (joinRoomResult.isType<GroupchatErrorType>()) {
sendEvent(
ErrorEvent(errorId: joinRoomResult.get<GroupchatErrorType>().value),
id: id,
);
}
} }
} }
@@ -1746,3 +1745,22 @@ Future<void> performConversationExited(
// Reset the active conversation // Reset the active conversation
cs.activeConversationJid = null; cs.activeConversationJid = null;
} }
Future<void> performGetMembers(
GetMembersForGroupchatCommand command, {
dynamic extra,
}) async {
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
final gs = GetIt.I.get<GroupchatService>();
// TODO(Unknown): Page this request
sendEvent(
GroupchatMembersResult(
members: await gs.getMembers(
JID.fromString(command.jid),
accountJid!,
),
),
id: extra as String,
);
}

View File

@@ -1,4 +1,5 @@
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/constants.dart'; import 'package:moxxyv2/service/database/constants.dart';
@@ -7,12 +8,16 @@ import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/xmpp_state.dart'; import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/error_types.dart'; import 'package:moxxyv2/shared/error_types.dart';
import 'package:moxxyv2/shared/models/groupchat.dart'; import 'package:moxxyv2/shared/models/groupchat.dart';
import 'package:moxxyv2/shared/models/groupchat_member.dart';
/// The value of the "var" attribute of the field containing the avatar hash (for Prosody). /// The value of the "var" attribute of the field containing the avatar hash (for Prosody).
const _prosodyAvatarHashFieldVar = const _prosodyAvatarHashFieldVar =
'{http://modules.prosody.im/mod_vcard_muc}avatar#sha1'; '{http://modules.prosody.im/mod_vcard_muc}avatar#sha1';
class GroupchatService { class GroupchatService {
/// Logger.
final Logger _log = Logger('GroupchatService');
/// Retrieves the information about a group chat room specified by the given /// Retrieves the information about a group chat room specified by the given
/// JID. /// JID.
/// Returns a [Future] that resolves to a [RoomInformation] object containing /// Returns a [Future] that resolves to a [RoomInformation] object containing
@@ -59,6 +64,35 @@ class GroupchatService {
), ),
); );
} else { } else {
// TODO(Unknown): Maybe be a bit smarter about it
final db = GetIt.I.get<DatabaseService>().database;
await db.delete(
groupchatMembersTable,
where: 'roomJid = ? AND accountJid = ?',
whereArgs: [muc.toString(), accountJid],
);
final state = (await mm.getRoomState(muc))!;
final members = List<GroupchatMember>.empty(growable: true);
_log.finest('Got ${state.members.length} members for $muc');
for (final rawMember in state.members.values) {
final member = GroupchatMember(
accountJid,
muc.toString(),
rawMember.nick,
rawMember.role,
rawMember.affiliation,
null,
null,
null,
);
await db.insert(
groupchatMembersTable,
member.toJson(),
);
members.add(member);
}
members.sort((a, b) => a.nick.compareTo(b.nick));
return Result( return Result(
GroupchatDetails( GroupchatDetails(
muc.toBare().toString(), muc.toBare().toString(),
@@ -157,4 +191,13 @@ class GroupchatService {
} }
return hashField.values.first; return hashField.values.first;
} }
Future<List<GroupchatMember>> getMembers(JID muc, String accountJid) async {
final result = await GetIt.I.get<DatabaseService>().database.query(
groupchatMembersTable,
where: 'roomJid = ? AND accountJid = ?',
whereArgs: [muc.toString(), accountJid],
);
return result.map(GroupchatMember.fromJson).toList();
}
} }

View File

@@ -2,7 +2,7 @@ import 'package:moxdns/moxdns.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart'; import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
class MoxxyTCPSocketWrapper extends TCPSocketWrapper { class MoxxyTCPSocketWrapper extends TCPSocketWrapper {
MoxxyTCPSocketWrapper() : super(); MoxxyTCPSocketWrapper() : super(false);
@override @override
Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async { Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async {

View File

@@ -1,6 +1,7 @@
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/moxxy_native.dart'; import 'package:moxxy_native/moxxy_native.dart';
import 'package:moxxyv2/shared/models/conversation.dart'; import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/groupchat_member.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/shared/models/omemo_device.dart'; import 'package:moxxyv2/shared/models/omemo_device.dart';
import 'package:moxxyv2/shared/models/preferences.dart'; import 'package:moxxyv2/shared/models/preferences.dart';

View File

@@ -0,0 +1,51 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moxxmpp/moxxmpp.dart';
part 'groupchat_member.freezed.dart';
part 'groupchat_member.g.dart';
class RoleTypeConverter extends JsonConverter<Role, String> {
const RoleTypeConverter();
@override
Role fromJson(String json) {
return Role.fromString(json);
}
@override
String toJson(Role object) {
return object.value;
}
}
class AffiliationTypeConverter extends JsonConverter<Affiliation, String> {
const AffiliationTypeConverter();
@override
Affiliation fromJson(String json) {
return Affiliation.fromString(json);
}
@override
String toJson(Affiliation object) {
return object.value;
}
}
@freezed
class GroupchatMember with _$GroupchatMember {
factory GroupchatMember(
String accountJid,
String roomJid,
String nick,
@RoleTypeConverter() Role role,
@AffiliationTypeConverter() Affiliation affiliation,
String? avatarPath,
String? avatarHash,
String? realJid,
) = _GroupchatMember;
/// JSON
factory GroupchatMember.fromJson(Map<String, dynamic> json) =>
_$GroupchatMemberFromJson(json);
}

View File

@@ -1,36 +1,154 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxy_native/moxxy_native.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/commands.dart';
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/bloc/server_info_bloc.dart';
import 'package:moxxyv2/ui/pages/profile/conversationheader.dart'; import 'package:moxxyv2/ui/pages/profile/conversationheader.dart';
import 'package:moxxyv2/ui/pages/profile/profile.dart'; import 'package:moxxyv2/ui/pages/profile/profile.dart';
import 'package:moxxyv2/ui/pages/profile/selfheader.dart'; import 'package:moxxyv2/ui/pages/profile/selfheader.dart';
import 'package:moxxyv2/ui/widgets/avatar.dart';
class ProfileView extends StatelessWidget { int affiliationToInt(Affiliation a) => switch (a) {
Affiliation.owner => 4,
Affiliation.admin => 3,
Affiliation.member => 2,
Affiliation.none => 1,
Affiliation.outcast => 0,
};
int affiliationSortingFunction(Affiliation a, Affiliation b) =>
affiliationToInt(a).compareTo(affiliationToInt(b));
int groupchatMemberSortingFunction(GroupchatMember a, GroupchatMember b) {
if (a.affiliation == b.affiliation) {
return b.nick.compareTo(a.nick);
}
return affiliationSortingFunction(b.affiliation, a.affiliation);
}
class ProfileView extends StatefulWidget {
const ProfileView(this.arguments, {super.key}); const ProfileView(this.arguments, {super.key});
final ProfileArguments arguments; final ProfileArguments arguments;
@override
ProfileViewState createState() => ProfileViewState();
}
class ProfileViewState extends State<ProfileView> {
List<GroupchatMember>? _members;
Future<void> _initStateAsync() async {
if (widget.arguments.type != ConversationType.groupchat) {
return;
}
final result = (await getForegroundService().send(
GetMembersForGroupchatCommand(
jid: widget.arguments.jid,
),
))! 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(() {
_members = members;
});
}
@override
void initState() {
super.initState();
_initStateAsync();
}
Widget _buildMemberList() {
if (_members == null) {
return const SliverToBoxAdapter(
child: CircularProgressIndicator(),
);
}
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,
},
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
ListView( CustomScrollView(
children: [ slivers: [
Padding( SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: arguments.isSelfProfile child: widget.arguments.isSelfProfile
? SelfProfileHeader(arguments) ? SelfProfileHeader(widget.arguments)
: const ConversationProfileHeader(), : const ConversationProfileHeader(),
), ),
),
if (widget.arguments.type == ConversationType.groupchat)
_buildMemberList(),
], ],
), ),
Positioned( Positioned(
top: 8, top: 8,
right: 8, right: 8,
child: Visibility( child: Visibility(
visible: arguments.isSelfProfile, visible: widget.arguments.isSelfProfile,
child: IconButton( child: IconButton(
color: Colors.white, color: Colors.white,
icon: const Icon(Icons.info_outline), icon: const Icon(Icons.info_outline),

View File

@@ -988,11 +988,9 @@ packages:
moxxmpp: moxxmpp:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/moxxmpp" path: "../moxxmpp/packages/moxxmpp"
ref: HEAD relative: true
resolved-ref: "814f99436b4bc5ff53136dfce73c2735b140b474" source: path
url: "https://codeberg.org/moxxy/moxxmpp.git"
source: git
version: "0.4.0" version: "0.4.0"
moxxmpp_color: moxxmpp_color:
dependency: "direct main" dependency: "direct main"
@@ -1005,11 +1003,10 @@ packages:
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
dependency: "direct main" dependency: "direct main"
description: description:
name: moxxmpp_socket_tcp path: "../moxxmpp/packages/moxxmpp_socket_tcp"
sha256: "32c2ee3316c6b87d9c6082e8ba35bb577700f7c0294587fb0b79e62c54d90577" relative: true
url: "https://git.polynom.me/api/packages/Moxxy/pub/" source: path
source: hosted version: "0.4.0"
version: "0.3.1"
moxxy_native: moxxy_native:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -67,7 +67,7 @@ dependencies:
version: 0.1.0 version: 0.1.0
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.1 version: 0.4.0
moxxy_native: moxxy_native:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.2 version: 0.3.2
@@ -121,18 +121,18 @@ dev_dependencies:
dependency_overrides: dependency_overrides:
# NOTE: Leave here for development purposes # NOTE: Leave here for development purposes
# moxxmpp: moxxmpp:
# path: ../moxxmpp/packages/moxxmpp path: ../moxxmpp/packages/moxxmpp
# moxxmpp_socket_tcp: moxxmpp_socket_tcp:
# path: ../moxxmpp/packages/moxxmpp_socket_tcp path: ../moxxmpp/packages/moxxmpp_socket_tcp
# omemo_dart: # omemo_dart:
# path: ../../Personal/omemo_dart # path: ../../Personal/omemo_dart
moxxmpp: #moxxmpp:
git: # git:
url: https://codeberg.org/moxxy/moxxmpp.git # url: https://codeberg.org/moxxy/moxxmpp.git
rev: 814f99436b4bc5ff53136dfce73c2735b140b474 # rev: 814f99436b4bc5ff53136dfce73c2735b140b474
path: packages/moxxmpp # path: packages/moxxmpp
# NOTE: Leave here for development purposes # NOTE: Leave here for development purposes
# moxxy_native: # moxxy_native: