Skip to content

Commit

Permalink
Merge pull request #2023 from nextcloud/feat/neon_talk/update-room-list
Browse files Browse the repository at this point in the history
  • Loading branch information
provokateurin authored May 14, 2024
2 parents fc36c42 + b42811d commit 2f6a6ff
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 13 deletions.
45 changes: 45 additions & 0 deletions packages/neon/neon_talk/lib/src/blocs/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:meta/meta.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/utils.dart';
import 'package:neon_talk/src/blocs/talk.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:nextcloud/spreed.dart' as spreed;
import 'package:rxdart/rxdart.dart';
Expand All @@ -16,6 +17,7 @@ import 'package:rxdart/subjects.dart';
abstract class TalkRoomBloc implements InteractiveBloc {
/// Creates a new Talk room bloc.
factory TalkRoomBloc({
required TalkBloc talkBloc,
required Account account,
required spreed.Room room,
}) = _TalkRoomBloc;
Expand All @@ -37,10 +39,52 @@ abstract class TalkRoomBloc implements InteractiveBloc {

class _TalkRoomBloc extends InteractiveBloc implements TalkRoomBloc {
_TalkRoomBloc({
required this.talkBloc,
required this.account,
required spreed.Room room,
}) : room = BehaviorSubject.seeded(Result.success(room)),
token = room.token {
messages.listen((result) {
if (!result.hasSuccessfulData) {
return;
}

// TODO: More room fields might be changed based on the received messages (e.g. room was renamed).

final lastMessage = result.requireData.firstOrNull;
if (lastMessage == null) {
return;
}

final value = this.room.value;
this.room.add(
value.copyWith(
data: value.requireData.rebuild(
(b) => b
..lastActivity = lastMessage.timestamp
..lastMessage = (
baseMessage: null,
builtListNever: null,
// TODO: This manual conversion is only necessary because the interface isn't used everywhere: https://github.com/nextcloud/neon/issues/1995
chatMessage: spreed.ChatMessage.fromJson(lastMessage.toJson()),
)
..lastCommonReadMessage = lastCommonRead.valueOrNull
// The following fields can be set because we know that any updates to the messages (sending/polling) updates the last read message.
..lastReadMessage = lastMessage.id
..unreadMention = false
..unreadMentionDirect = false
..unreadMessages = 0,
),
),
);
});

this.room.listen((result) {
if (result.hasSuccessfulData) {
talkBloc.updateRoom(result.requireData);
}
});

unawaited(() async {
while (pollLoop) {
final lastKnownMessageId =
Expand Down Expand Up @@ -86,6 +130,7 @@ class _TalkRoomBloc extends InteractiveBloc implements TalkRoomBloc {
@override
final log = Logger('TalkRoomBloc');

final TalkBloc talkBloc;
final Account account;
final String token;
bool pollLoop = true;
Expand Down
21 changes: 21 additions & 0 deletions packages/neon/neon_talk/lib/src/blocs/talk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ abstract class TalkBloc implements InteractiveBloc {
/// Creates a new Talk room.
void createRoom(spreed.RoomType type, String? roomName, core.AutocompleteResult? invite);

/// Updates a single room with new data.
void updateRoom(spreed.Room room);

/// The list of rooms.
BehaviorSubject<Result<BuiltList<spreed.Room>>> get rooms;

Expand All @@ -45,12 +48,15 @@ class _TalkBloc extends InteractiveBloc implements TalkBloc {
});

unawaited(refresh());

timer = TimerBloc().registerTimer(const Duration(seconds: 30), refresh);
}

@override
final log = Logger('TalkBloc');

final Account account;
late final NeonTimer timer;

@override
final rooms = BehaviorSubject();
Expand All @@ -60,6 +66,7 @@ class _TalkBloc extends InteractiveBloc implements TalkBloc {

@override
void dispose() {
timer.cancel();
unawaited(rooms.close());
unawaited(unreadCounter.close());
super.dispose();
Expand Down Expand Up @@ -94,4 +101,18 @@ class _TalkBloc extends InteractiveBloc implements TalkBloc {
);
});
}

@override
void updateRoom(spreed.Room room) {
final value = rooms.valueOrNull;
if (value == null || !value.hasData) {
return;
}

rooms.add(
value.copyWith(
data: value.requireData.map((r) => r.id == room.id ? room : r).toBuiltList(),
),
);
}
}
1 change: 1 addition & 0 deletions packages/neon/neon_talk/lib/src/pages/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class _TalkMainPageState extends State<TalkMainPage> {
MaterialPageRoute<void>(
builder: (context) => NeonProvider(
create: (_) => TalkRoomBloc(
talkBloc: bloc,
account: account,
room: room,
),
Expand Down
22 changes: 22 additions & 0 deletions packages/neon/neon_talk/test/bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mocktail/mocktail.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/testing.dart';
Expand Down Expand Up @@ -134,4 +135,25 @@ void main() {
),
);
});

test('updateRoom', () async {
expect(
bloc.rooms.transformResult((e) => BuiltList<String>(e.map((r) => r.displayName))),
emitsInOrder([
Result<BuiltList<String>>.loading(),
Result.success(BuiltList<String>(['0', '1', '2'])),
Result.success(BuiltList<String>(['update', '1', '2'])),
]),
);

// The delay is necessary to avoid a race condition with loading twice at the same time
await Future<void>.delayed(const Duration(milliseconds: 1));

final room = MockRoom();
when(() => room.id).thenReturn(0);
when(() => room.displayName).thenReturn('update');
when(() => room.unreadMessages).thenReturn(0);

bloc.updateRoom(room);
});
}
58 changes: 46 additions & 12 deletions packages/neon/neon_talk/test/room_bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/testing.dart';
import 'package:neon_talk/src/blocs/room.dart';
import 'package:neon_talk/src/blocs/talk.dart';

import 'testing.dart';

Expand Down Expand Up @@ -88,7 +89,12 @@ Account mockTalkAccount() {

void main() {
late Account account;
late TalkRoomBloc bloc;
late TalkBloc talkBloc;
late TalkRoomBloc roomBloc;

setUpAll(() {
registerFallbackValue(MockRoom());
});

setUp(() {
FakeNeonStorage.setup();
Expand All @@ -100,7 +106,9 @@ void main() {
when(() => room.lastMessage).thenReturn((baseMessage: null, builtListNever: null, chatMessage: null));

account = mockTalkAccount();
bloc = TalkRoomBloc(
talkBloc = MockTalkBloc();
roomBloc = TalkRoomBloc(
talkBloc: talkBloc,
account: account,
room: room,
);
Expand All @@ -109,22 +117,24 @@ void main() {
tearDown(() async {
// Wait for all events to be processed
await Future<void>.delayed(const Duration(milliseconds: 1));
bloc.dispose();
roomBloc.dispose();
});

test('refresh', () async {
expect(
bloc.room.transformResult((e) => e.token),
roomBloc.room.transformResult((e) => e.token),
emitsInOrder([
Result.success('abcd').asLoading(),
Result.success('abcd'),
Result.success('abcd'),
Result.success('abcd').asLoading(),
Result.success('abcd'),
Result.success('abcd'),
]),
);

expect(
bloc.messages.transformResult((e) => BuiltList<int>(e.map((m) => m.id))),
roomBloc.messages.transformResult((e) => BuiltList<int>(e.map((m) => m.id))),
emitsInOrder([
Result<BuiltList<int>>.loading(),
Result.success(BuiltList<int>([2, 1, 0])),
Expand All @@ -134,48 +144,72 @@ void main() {
);

expect(
bloc.lastCommonRead,
roomBloc.lastCommonRead,
emitsInOrder([0, 0]),
);

// The delay is necessary to avoid a race condition with loading twice at the same time
await Future<void>.delayed(const Duration(milliseconds: 1));
await bloc.refresh();
await roomBloc.refresh();

verify(() => talkBloc.updateRoom(any())).called(4);
});

test('sendMessage', () async {
expect(
bloc.messages.transformResult((e) => BuiltList<int>(e.map((m) => m.id))),
roomBloc.messages.transformResult((e) => BuiltList<int>(e.map((m) => m.id))),
emitsInOrder([
Result<BuiltList<int>>.loading(),
Result.success(BuiltList<int>([2, 1, 0])),
Result.success(BuiltList<int>([3, 2, 1, 0])),
]),
);
expect(
roomBloc.room.transformResult((e) => e.lastMessage.chatMessage?.id),
emitsInOrder([
Result<int>.loading(),
Result.success(null),
Result.success(2),
Result.success(3),
]),
);

expect(
bloc.lastCommonRead,
roomBloc.lastCommonRead,
emitsInOrder([0, 1]),
);

// The delay is necessary to avoid a race condition with loading twice at the same time
await Future<void>.delayed(const Duration(milliseconds: 1));
bloc.sendMessage('');
roomBloc.sendMessage('');

verify(() => talkBloc.updateRoom(any())).called(3);
});

test('polling', () async {
expect(
bloc.messages.transformResult((e) => BuiltList<int>(e.map((m) => m.id))),
roomBloc.messages.transformResult((e) => BuiltList<int>(e.map((m) => m.id))),
emitsInOrder([
Result<BuiltList<int>>.loading(),
Result.success(BuiltList<int>([2, 1, 0])),
Result.success(BuiltList<int>([4, 3, 2, 1, 0])),
]),
);
expect(
roomBloc.room.transformResult((e) => e.lastMessage.chatMessage?.id),
emitsInOrder([
Result<int>.loading(),
Result.success(null),
Result.success(2),
Result.success(4),
]),
);

expect(
bloc.lastCommonRead,
roomBloc.lastCommonRead,
emitsInOrder([0, 0]),
);

verify(() => talkBloc.updateRoom(any())).called(1);
});
}
2 changes: 1 addition & 1 deletion packages/neon/neon_talk/test/testing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Map<String, dynamic> getRoom({
'canStartCall': false,
'defaultPermissions': 0,
'description': '',
'displayName': '',
'displayName': (id ?? 0).toString(),
'hasCall': false,
'hasPassword': false,
'id': id ?? 0,
Expand Down

0 comments on commit 2f6a6ff

Please sign in to comment.