Skip to content

Commit 7e995b5

Browse files
committed
feat(neon_talk): Add room list
Signed-off-by: provokateurin <kate@provokateurin.de>
1 parent bea6676 commit 7e995b5

17 files changed

+580
-5
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"@@locale": "en"
2+
"@@locale": "en",
3+
"actorSelf": "You"
34
}

packages/neon/neon_talk/lib/l10n/localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ abstract class TalkLocalizations {
8888

8989
/// A list of this localizations delegate's supported locales.
9090
static const List<Locale> supportedLocales = <Locale>[Locale('en')];
91+
92+
/// No description provided for @actorSelf.
93+
///
94+
/// In en, this message translates to:
95+
/// **'You'**
96+
String get actorSelf;
9197
}
9298

9399
class _TalkLocalizationsDelegate extends LocalizationsDelegate<TalkLocalizations> {

packages/neon/neon_talk/lib/l10n/localizations_en.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ import 'localizations.dart';
33
/// The translations for English (`en`).
44
class TalkLocalizationsEn extends TalkLocalizations {
55
TalkLocalizationsEn([String locale = 'en']) : super(locale);
6+
7+
@override
8+
String get actorSelf => 'You';
69
}

packages/neon/neon_talk/lib/src/app.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:neon_talk/src/options.dart';
99
import 'package:neon_talk/src/pages/main.dart';
1010
import 'package:neon_talk/src/routes.dart';
1111
import 'package:nextcloud/nextcloud.dart';
12+
import 'package:rxdart/rxdart.dart';
1213

1314
/// Implementation of the server `talk` app.
1415
@experimental
@@ -39,4 +40,7 @@ class TalkApp extends AppImplementation<TalkBloc, TalkOptions> {
3940

4041
@override
4142
final RouteBase route = $talkAppRoute;
43+
44+
@override
45+
BehaviorSubject<int> getUnreadCounter(TalkBloc bloc) => bloc.unreadCounter;
4246
}
Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,70 @@
1+
import 'dart:async';
2+
3+
import 'package:built_collection/built_collection.dart';
14
import 'package:meta/meta.dart';
25
import 'package:neon_framework/blocs.dart';
36
import 'package:neon_framework/models.dart';
7+
import 'package:neon_framework/utils.dart';
8+
import 'package:nextcloud/spreed.dart' as spreed;
9+
import 'package:rxdart/rxdart.dart';
410

511
/// Bloc for fetching Talk rooms
612
sealed class TalkBloc implements InteractiveBloc {
713
/// Creates a new Talk Bloc instance.
814
@internal
915
factory TalkBloc(Account account) => _TalkBloc(account);
16+
17+
/// The list of rooms.
18+
BehaviorSubject<Result<BuiltList<spreed.Room>>> get rooms;
19+
20+
/// The total number of unread messages.
21+
BehaviorSubject<int> get unreadCounter;
1022
}
1123

1224
class _TalkBloc extends InteractiveBloc implements TalkBloc {
13-
_TalkBloc(this.account);
25+
_TalkBloc(this.account) {
26+
rooms.listen((result) {
27+
if (!result.hasData) {
28+
return;
29+
}
30+
31+
var unread = 0;
32+
for (final room in result.requireData) {
33+
unread += room.unreadMessages;
34+
}
35+
unreadCounter.add(unread);
36+
});
37+
38+
unawaited(refresh());
39+
}
1440

1541
final Account account;
1642

1743
@override
18-
Future<void> refresh() async {}
44+
final rooms = BehaviorSubject();
45+
46+
@override
47+
final unreadCounter = BehaviorSubject();
48+
49+
@override
50+
void dispose() {
51+
unawaited(rooms.close());
52+
unawaited(unreadCounter.close());
53+
super.dispose();
54+
}
55+
56+
@override
57+
Future<void> refresh() async {
58+
await RequestManager.instance.wrapNextcloud(
59+
account: account,
60+
cacheKey: 'talk-rooms',
61+
subject: rooms,
62+
rawResponse: account.client.spreed.room.getRoomsRaw(),
63+
unwrap: (response) => BuiltList(
64+
response.body.ocs.data.rebuild(
65+
(b) => b.sort((a, b) => b.lastActivity.compareTo(a.lastActivity)),
66+
),
67+
),
68+
);
69+
}
1970
}
Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,81 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/material.dart';
4+
import 'package:neon_framework/blocs.dart';
5+
import 'package:neon_framework/utils.dart';
6+
import 'package:neon_framework/widgets.dart';
7+
import 'package:neon_talk/src/blocs/talk.dart';
8+
import 'package:neon_talk/src/widgets/message_preview.dart';
9+
import 'package:neon_talk/src/widgets/unread_indicator.dart';
10+
import 'package:nextcloud/spreed.dart' as spreed;
211

312
/// The main page displaying the chat list.
4-
class TalkMainPage extends StatelessWidget {
13+
class TalkMainPage extends StatefulWidget {
514
/// Creates a new Talk main page.
615
const TalkMainPage({super.key});
716

817
@override
9-
Widget build(BuildContext context) => const Placeholder();
18+
State<TalkMainPage> createState() => _TalkMainPageState();
19+
}
20+
21+
class _TalkMainPageState extends State<TalkMainPage> {
22+
late String actorId;
23+
late TalkBloc bloc;
24+
late StreamSubscription<Object> errorsSubscription;
25+
26+
@override
27+
void initState() {
28+
super.initState();
29+
30+
actorId = NeonProvider.of<AccountsBloc>(context).activeAccount.value!.username;
31+
bloc = NeonProvider.of<TalkBloc>(context);
32+
bloc.errors.listen((error) {
33+
NeonError.showSnackbar(context, error);
34+
});
35+
}
36+
37+
@override
38+
void dispose() {
39+
unawaited(errorsSubscription.cancel());
40+
super.dispose();
41+
}
42+
43+
@override
44+
Widget build(BuildContext context) => ResultBuilder.behaviorSubject(
45+
subject: bloc.rooms,
46+
builder: (context, rooms) => NeonListView(
47+
scrollKey: 'talk-rooms',
48+
isLoading: rooms.isLoading,
49+
error: rooms.error,
50+
onRefresh: bloc.refresh,
51+
itemCount: rooms.data?.length ?? 0,
52+
itemBuilder: (context, index) => buildRoom(rooms.requireData[index]),
53+
),
54+
);
55+
56+
Widget buildRoom(spreed.Room room) {
57+
Widget? subtitle;
58+
Widget? trailing;
59+
60+
final lastChatMessage = room.lastMessage.chatMessage;
61+
if (lastChatMessage != null) {
62+
subtitle = TalkMessagePreview(
63+
actorId: actorId,
64+
roomType: spreed.RoomType.fromValue(room.type),
65+
chatMessage: lastChatMessage,
66+
);
67+
}
68+
69+
if (room.unreadMessages > 0) {
70+
trailing = TalkUnreadIndicator(
71+
room: room,
72+
);
73+
}
74+
75+
return ListTile(
76+
title: Text(room.displayName),
77+
subtitle: subtitle,
78+
trailing: trailing,
79+
);
80+
}
1081
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:nextcloud/spreed.dart' as spreed;
3+
4+
/// Builds a [TextSpan] for the given [chatMessage].
5+
TextSpan buildChatMessage({
6+
required spreed.ChatMessage chatMessage,
7+
}) =>
8+
TextSpan(
9+
text: chatMessage.message,
10+
);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:neon_talk/l10n/localizations.dart';
3+
import 'package:neon_talk/src/utils/message.dart';
4+
import 'package:nextcloud/spreed.dart' as spreed;
5+
6+
/// Displays a preview of the [chatMessage] including the display name of the sender.
7+
class TalkMessagePreview extends StatelessWidget {
8+
/// Creates a new Talk message preview.
9+
const TalkMessagePreview({
10+
required this.actorId,
11+
required this.roomType,
12+
required this.chatMessage,
13+
super.key,
14+
});
15+
16+
/// ID of the current actor.
17+
final String actorId;
18+
19+
/// Type of the room
20+
final spreed.RoomType roomType;
21+
22+
/// The chat message to preview.
23+
final spreed.ChatMessage chatMessage;
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
String? actorName;
28+
if (chatMessage.actorId == actorId) {
29+
actorName = TalkLocalizations.of(context).actorSelf;
30+
} else if (!roomType.isSingleUser) {
31+
actorName = chatMessage.actorDisplayName;
32+
}
33+
34+
return RichText(
35+
maxLines: 1,
36+
overflow: TextOverflow.ellipsis,
37+
text: TextSpan(
38+
style: TextStyle(
39+
color: Theme.of(context).colorScheme.onBackground,
40+
),
41+
children: [
42+
if (actorName != null)
43+
TextSpan(
44+
text: '$actorName: ',
45+
style: const TextStyle(
46+
fontWeight: FontWeight.bold,
47+
),
48+
),
49+
buildChatMessage(
50+
chatMessage: chatMessage,
51+
),
52+
],
53+
),
54+
);
55+
}
56+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:nextcloud/spreed.dart' as spreed;
3+
4+
/// Displays the number of unread messages and whether the user was mentioned for a given [room].
5+
class TalkUnreadIndicator extends StatelessWidget {
6+
/// Creates a new Talk unread indicator.
7+
const TalkUnreadIndicator({
8+
required this.room,
9+
super.key,
10+
});
11+
12+
/// The room that the indicator will display unread messages and mentions for.
13+
final spreed.Room room;
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
assert(room.unreadMessages > 0, 'Need at least on unread message');
18+
19+
final colorScheme = Theme.of(context).colorScheme;
20+
21+
final highlight = room.unreadMention || spreed.RoomType.fromValue(room.type).isSingleUser;
22+
final backgroundColor = highlight ? colorScheme.primaryContainer : colorScheme.background;
23+
final textColor = highlight ? colorScheme.onPrimaryContainer : colorScheme.onBackground;
24+
25+
Widget? avatar;
26+
if (room.unreadMentionDirect) {
27+
avatar = Icon(
28+
Icons.alternate_email,
29+
size: 20,
30+
color: textColor,
31+
);
32+
}
33+
34+
return Chip(
35+
shape: RoundedRectangleBorder(
36+
borderRadius: const BorderRadius.all(Radius.circular(50)),
37+
side: BorderSide(
38+
color: colorScheme.primaryContainer,
39+
),
40+
),
41+
padding: const EdgeInsets.all(2),
42+
backgroundColor: backgroundColor,
43+
avatar: avatar,
44+
label: Text(
45+
room.unreadMessages.toString(),
46+
style: TextStyle(
47+
fontWeight: FontWeight.bold,
48+
fontFamily: 'monospace',
49+
color: textColor,
50+
),
51+
),
52+
);
53+
}
54+
}

packages/neon/neon_talk/pubspec.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ dependencies:
1919
url: https://github.com/nextcloud/neon
2020
path: packages/neon_framework
2121
nextcloud: ^5.0.2
22+
rxdart: ^0.27.0
2223

2324
dev_dependencies:
2425
build_runner: ^2.4.8
26+
flutter_test:
27+
sdk: flutter
2528
go_router_builder: ^2.4.1
29+
http: ^1.2.1
30+
mocktail: ^1.0.3
2631
neon_lints:
2732
git:
2833
url: https://github.com/nextcloud/neon

0 commit comments

Comments
 (0)