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

View File

@@ -18,6 +18,7 @@ const omemoRatchetsTable = 'OmemoRatchets';
const omemoTrustTable = 'OmemoTrustTable';
const notificationsTable = 'Notifications';
const groupchatTable = 'Groupchat';
const groupchatMembersTable = 'GroupchatMembers';
const typeString = 0;
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_remove_subscriptions.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/xmpp_state.dart';
import 'package:path/path.dart' as path;
@@ -109,6 +110,7 @@ const List<Migration<DatabaseMigrationData>> migrations = [
Migration(46, upgradeFromV45ToV46),
Migration(47, upgradeFromV46ToV47),
Migration(48, upgradeFromV47ToV48),
Migration(49, upgradeFromV48ToV49),
];
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,
),
EventTypeMatcher<ExitConversationCommand>(performConversationExited),
EventTypeMatcher<GetMembersForGroupchatCommand>(performGetMembers),
]);
GetIt.I.registerSingleton<EventHandler>(handler);
@@ -336,13 +337,10 @@ Future<void> performAddConversation(
);
if (conversation!.type == ConversationType.groupchat) {
await GetIt.I
.get<XmppConnection>()
.getManagerById<MUCManager>(mucManager)!
.joinRoom(
await GetIt.I.get<GroupchatService>().joinRoom(
JID.fromString(conversation.jid),
accountJid,
conversation.groupchatDetails!.nick,
maxHistoryStanzas: 0,
);
}
}
@@ -1631,18 +1629,6 @@ Future<void> performJoinGroupchat(
);
} else {
// 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(
jid,
accountJid,
@@ -1682,6 +1668,19 @@ Future<void> performJoinGroupchat(
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
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:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.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/shared/error_types.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).
const _prosodyAvatarHashFieldVar =
'{http://modules.prosody.im/mod_vcard_muc}avatar#sha1';
class GroupchatService {
/// Logger.
final Logger _log = Logger('GroupchatService');
/// Retrieves the information about a group chat room specified by the given
/// JID.
/// Returns a [Future] that resolves to a [RoomInformation] object containing
@@ -59,6 +64,35 @@ class GroupchatService {
),
);
} 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(
GroupchatDetails(
muc.toBare().toString(),
@@ -157,4 +191,13 @@ class GroupchatService {
}
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';
class MoxxyTCPSocketWrapper extends TCPSocketWrapper {
MoxxyTCPSocketWrapper() : super();
MoxxyTCPSocketWrapper() : super(false);
@override
Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async {

View File

@@ -1,6 +1,7 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/moxxy_native.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/omemo_device.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_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/pages/profile/conversationheader.dart';
import 'package:moxxyv2/ui/pages/profile/profile.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});
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
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
ListView(
children: [
Padding(
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: arguments.isSelfProfile
? SelfProfileHeader(arguments)
child: widget.arguments.isSelfProfile
? SelfProfileHeader(widget.arguments)
: const ConversationProfileHeader(),
),
),
if (widget.arguments.type == ConversationType.groupchat)
_buildMemberList(),
],
),
Positioned(
top: 8,
right: 8,
child: Visibility(
visible: arguments.isSelfProfile,
visible: widget.arguments.isSelfProfile,
child: IconButton(
color: Colors.white,
icon: const Icon(Icons.info_outline),

View File

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

View File

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