feat(ui,service): Get a basic participant list going
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
28
lib/service/database/migrations/0004_groupchat_members.dart
Normal file
28
lib/service/database/migrations/0004_groupchat_members.dart
Normal 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)',
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
51
lib/shared/models/groupchat_member.dart
Normal file
51
lib/shared/models/groupchat_member.dart
Normal 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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
17
pubspec.lock
17
pubspec.lock
@@ -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:
|
||||
|
||||
20
pubspec.yaml
20
pubspec.yaml
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user