Skip to content

Commit 8dc95cf

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

17 files changed

+573
-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: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,71 @@
1+
import 'dart:async';
2+
3+
import 'package:built_collection/built_collection.dart';
4+
import 'package:collection/collection.dart';
15
import 'package:meta/meta.dart';
26
import 'package:neon_framework/blocs.dart';
37
import 'package:neon_framework/models.dart';
8+
import 'package:neon_framework/utils.dart';
9+
import 'package:nextcloud/spreed.dart' as spreed;
10+
import 'package:rxdart/rxdart.dart';
411

512
/// Bloc for fetching Talk rooms
613
sealed class TalkBloc implements InteractiveBloc {
714
/// Creates a new Talk Bloc instance.
815
@internal
916
factory TalkBloc(Account account) => _TalkBloc(account);
17+
18+
/// The list of rooms.
19+
BehaviorSubject<Result<BuiltList<spreed.Room>>> get rooms;
20+
21+
/// The total number of unread messages.
22+
BehaviorSubject<int> get unreadCounter;
1023
}
1124

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

1542
final Account account;
1643

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

310
/// The main page displaying the chat list.
4-
class TalkMainPage extends StatelessWidget {
11+
class TalkMainPage extends StatefulWidget {
512
/// Creates a new Talk main page.
613
const TalkMainPage({super.key});
714

815
@override
9-
Widget build(BuildContext context) => const Placeholder();
16+
State<TalkMainPage> createState() => _TalkMainPageState();
17+
}
18+
19+
class _TalkMainPageState extends State<TalkMainPage> {
20+
late String actorId;
21+
late TalkBloc bloc;
22+
23+
@override
24+
void initState() {
25+
super.initState();
26+
27+
actorId = NeonProvider.of<AccountsBloc>(context).activeAccount.value!.username;
28+
bloc = NeonProvider.of<TalkBloc>(context);
29+
30+
bloc.errors.listen((error) {
31+
NeonError.showSnackbar(context, error);
32+
});
33+
}
34+
35+
@override
36+
Widget build(BuildContext context) => ResultBuilder.behaviorSubject(
37+
subject: bloc.rooms,
38+
builder: (context, rooms) => NeonListView(
39+
scrollKey: 'talk-rooms',
40+
isLoading: rooms.isLoading,
41+
error: rooms.error,
42+
onRefresh: bloc.refresh,
43+
itemCount: rooms.data?.length ?? 0,
44+
itemBuilder: (context, index) => buildRoom(rooms.requireData[index]),
45+
),
46+
);
47+
48+
Widget buildRoom(spreed.Room room) {
49+
final roomType = spreed.RoomType.fromValue(room.type);
50+
return ListTile(
51+
title: Text(room.displayName),
52+
subtitle: room.lastMessage.chatMessage != null
53+
? TalkMessagePreview(
54+
actorId: actorId,
55+
roomType: roomType,
56+
chatMessage: room.lastMessage.chatMessage!,
57+
)
58+
: null,
59+
trailing: room.unreadMessages > 0
60+
? TalkUnreadIndicator(
61+
room: room,
62+
)
63+
: null,
64+
);
65+
}
1066
}
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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
children: [
39+
if (actorName != null)
40+
TextSpan(
41+
children: [
42+
TextSpan(
43+
text: actorName,
44+
style: const TextStyle(
45+
fontWeight: FontWeight.bold,
46+
),
47+
),
48+
const TextSpan(
49+
text: ': ',
50+
),
51+
],
52+
),
53+
buildChatMessage(
54+
chatMessage: chatMessage,
55+
),
56+
],
57+
),
58+
);
59+
}
60+
}
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: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+
return Row(
26+
mainAxisSize: MainAxisSize.min,
27+
children: [
28+
Chip(
29+
shape: RoundedRectangleBorder(
30+
borderRadius: const BorderRadius.all(Radius.circular(50)),
31+
side: BorderSide(
32+
color: colorScheme.primaryContainer,
33+
),
34+
),
35+
padding: const EdgeInsets.all(2),
36+
backgroundColor: backgroundColor,
37+
avatar: room.unreadMentionDirect
38+
? Icon(
39+
Icons.alternate_email,
40+
size: 20,
41+
color: textColor,
42+
)
43+
: null,
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+
);
55+
}
56+
}

packages/neon/neon_talk/pubspec.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ environment:
88

99
dependencies:
1010
built_collection: ^5.0.0
11+
collection: ^1.0.0
1112
flutter:
1213
sdk: flutter
1314
flutter_localizations:
@@ -19,10 +20,15 @@ dependencies:
1920
url: https://github.com/nextcloud/neon
2021
path: packages/neon_framework
2122
nextcloud: ^5.0.2
23+
rxdart: ^0.27.0
2224

2325
dev_dependencies:
2426
build_runner: ^2.4.8
27+
flutter_test:
28+
sdk: flutter
2529
go_router_builder: ^2.4.1
30+
http: ^1.2.1
31+
mocktail: ^1.0.3
2632
neon_lints:
2733
git:
2834
url: https://github.com/nextcloud/neon

0 commit comments

Comments
 (0)