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-2165 Support store members in local database #71

45 changes: 45 additions & 0 deletions doc/adr/0005-support-store-members-in-hive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 5. Support store members in hive

Date: 2024-12-23

## Status

Accepted

- Issue: [#2165](https://github.com/linagora/twake-on-matrix/issues/2165)

## Context

- Not all of the members are displayed in the drop-down list
- The members only store in the memory, so when the user refreshes the page, the members are lost.

## Decision

- Store the members in the hive to keep the members when the user refreshes the page.
- Add some properties to request the members from the server.

```dart

Future<List<User>> requestParticipantsFromServer({
List<Membership> membershipFilter = displayMembershipsFilter,
bool suppressWarning = false,
bool cache = true,
String? at,
Membership? membership,
Membership? notMembership,
}) {}

```

- `at`: The point in time (pagination token) to return members for in the room.
This token can be obtained from a prev_batch token returned for each room by the sync API.
Defaults to the current state of the room, as determined by the server.

- `membership`: The kind of membership to filter for. Defaults to no filtering if unspecified.
When specified alongside `not_membership`, the two parameters create an ‘or’ condition: either the membership is the same as `membership` or is not the same as `not_membership`.

- `notMembership`: The kind of membership to exclude from the results. Defaults to no filtering if unspecified.

## Consequences

- The members are stored in the hive, so the members are not lost when the user refreshes the page.
2 changes: 2 additions & 0 deletions lib/src/database/database_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ abstract class DatabaseApi {

Future<List<User>> getUsers(Room room);

Future<void> storeUsers(List<User> users, Room room);

Future<List<Event>> getEventList(
Room room, {
int start = 0,
Expand Down
9 changes: 9 additions & 0 deletions lib/src/database/hive_collections_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
return users;
}

@override
Future<void> storeUsers(List<User> users, Room room) async {
for (final user in users) {
final key = TupleKey(room.id, user.id).toString();
await _roomMembersBox.put(key, user.toJson());
}
return;
}

@override
Future<int> insertClient(
String name,
Expand Down
9 changes: 9 additions & 0 deletions lib/src/database/hive_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,15 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
return users;
}

@override
Future<void> storeUsers(List<User> users, Room room) async {
for (final user in users) {
final key = MultiKey(room.id, user.id).toString();
await _roomMembersBox.put(key, user.toJson());
}
return Future.value();
}

@override
Future<void> insertClient(
String name,
Expand Down
64 changes: 43 additions & 21 deletions lib/src/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1337,12 +1337,13 @@ class Room {
/// List `membershipFilter` defines with what membership do you want the
/// participants, default set to
/// [[Membership.join, Membership.invite, Membership.knock]]
List<User> getParticipants(
[List<Membership> membershipFilter = const [
Membership.join,
Membership.invite,
Membership.knock,
]]) {
List<User> getParticipants({
List<Membership> membershipFilter = const [
Membership.join,
Membership.invite,
Membership.knock,
],
}) {
final members = states[EventTypes.RoomMember];
if (members != null) {
return members.entries
Expand All @@ -1363,11 +1364,15 @@ class Room {
/// [[Membership.join, Membership.invite, Membership.knock]]
/// Set [cache] to `false` if you do not want to cache the users in memory
/// for this session which is highly recommended for large public rooms.
Future<List<User>> requestParticipants(
[List<Membership> membershipFilter = displayMembershipsFilter,
bool suppressWarning = false,
bool cache = true]) async {
if (!participantListComplete && partial) {
Future<List<User>> requestParticipants({
List<Membership> membershipFilter = displayMembershipsFilter,
bool suppressWarning = false,
bool cache = true,
String? at,
Membership? membership,
Membership? notMembership,
}) async {
if (!participantListComplete || partial) {
// we aren't fully loaded, maybe the users are in the database
final users = await client.database?.getUsers(this) ?? [];
for (final user in users) {
Expand All @@ -1378,20 +1383,27 @@ class Room {
// Do not request users from the server if we have already done it
// in this session or have a complete list locally.
if (_requestedParticipants || participantListComplete) {
return getParticipants(membershipFilter);
return getParticipants(membershipFilter: membershipFilter);
}

return await requestParticipantsFromServer(
membershipFilter,
suppressWarning,
cache,
membershipFilter: membershipFilter,
suppressWarning: suppressWarning,
cache: cache,
at: at,
membership: membership,
notMembership: notMembership,
);
}

Future<List<User>> requestParticipantsFromServer(
[List<Membership> membershipFilter = displayMembershipsFilter,
bool suppressWarning = false,
bool cache = true]) async {
Future<List<User>> requestParticipantsFromServer({
List<Membership> membershipFilter = displayMembershipsFilter,
bool suppressWarning = false,
bool cache = true,
String? at,
Membership? membership,
Membership? notMembership,
}) async {
final memberCount = summary.mJoinedMemberCount;
if (!suppressWarning && cache && memberCount != null && memberCount > 100) {
Logs().w('''
Expand All @@ -1401,15 +1413,25 @@ class Room {
''');
}

final matrixEvents = await client.getMembersByRoom(id);
final matrixEvents = await client.getMembersByRoom(
id,
at: at,
membership: membership,
notMembership: notMembership,
);
final users = matrixEvents
?.map((e) => Event.fromMatrixEvent(e, this).asUser)
.toList() ??
[];

if (cache) {
for (final user in users) {
setState(user); // at *least* cache this in-memory
setState(user);
}
try {
await client.database?.storeUsers(users, this);
} catch (e) {
Logs().w('Room::requestParticipantsFromServer: Unable to store users in the database', e);
}
}

Expand Down
6 changes: 5 additions & 1 deletion lib/src/utils/pushrule_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,11 @@ class PushruleEvaluator {
}

EvaluatedPushRuleAction match(Event event) {
final memberCount = event.room.getParticipants([Membership.join]).length;
final memberCount = event.room.getParticipants(
membershipFilter: [
Membership.join,
],
).length;
final displayName = event.room
.unsafeGetUserFromMemoryOrFallback(event.room.client.userID!)
.displayName;
Expand Down
19 changes: 19 additions & 0 deletions test/database_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ void testDatabase(
Room(id: '!testroom:example.com', client: Client('testclient')));
expect(users.isEmpty, true);
});
test('storeUsers', () async {
final room =
Room(id: '!testroom:example.com', client: Client('testclient'));
await database.storeUsers(
nqhhdev marked this conversation as resolved.
Show resolved Hide resolved
[
User(
'@bob:example.org',
displayName: 'Bob',
avatarUrl: 'mxc://example.com',
room: room,
)
],
Room(id: '!testroom:example.com', client: Client('testclient')),
);
final users = await database.getUsers(
Room(id: '!testroom:example.com', client: Client('testclient')),
);
expect(users.single.id, '@bob:example.org');
});
test('removeEvent', () async {
await database.removeEvent('\$event:example.com', '!testroom:example.com');
final event = await database.getEventById('\$event:example.com',
Expand Down
Loading