Skip to content

Commit

Permalink
Merge pull request #1657 from nextcloud/feat/neon_talk/room-list
Browse files Browse the repository at this point in the history
  • Loading branch information
provokateurin authored Feb 26, 2024
2 parents bea6676 + 950fd36 commit 8c19800
Show file tree
Hide file tree
Showing 17 changed files with 580 additions and 5 deletions.
3 changes: 2 additions & 1 deletion packages/neon/neon_talk/lib/l10n/en.arb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"@@locale": "en"
"@@locale": "en",
"actorSelf": "You"
}
6 changes: 6 additions & 0 deletions packages/neon/neon_talk/lib/l10n/localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ abstract class TalkLocalizations {

/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[Locale('en')];

/// No description provided for @actorSelf.
///
/// In en, this message translates to:
/// **'You'**
String get actorSelf;
}

class _TalkLocalizationsDelegate extends LocalizationsDelegate<TalkLocalizations> {
Expand Down
3 changes: 3 additions & 0 deletions packages/neon/neon_talk/lib/l10n/localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ import 'localizations.dart';
/// The translations for English (`en`).
class TalkLocalizationsEn extends TalkLocalizations {
TalkLocalizationsEn([String locale = 'en']) : super(locale);

@override
String get actorSelf => 'You';
}
4 changes: 4 additions & 0 deletions packages/neon/neon_talk/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:neon_talk/src/options.dart';
import 'package:neon_talk/src/pages/main.dart';
import 'package:neon_talk/src/routes.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:rxdart/rxdart.dart';

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

@override
final RouteBase route = $talkAppRoute;

@override
BehaviorSubject<int> getUnreadCounter(TalkBloc bloc) => bloc.unreadCounter;
}
55 changes: 53 additions & 2 deletions packages/neon/neon_talk/lib/src/blocs/talk.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,70 @@
import 'dart:async';

import 'package:built_collection/built_collection.dart';
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:nextcloud/spreed.dart' as spreed;
import 'package:rxdart/rxdart.dart';

/// Bloc for fetching Talk rooms
sealed class TalkBloc implements InteractiveBloc {
/// Creates a new Talk Bloc instance.
@internal
factory TalkBloc(Account account) => _TalkBloc(account);

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

/// The total number of unread messages.
BehaviorSubject<int> get unreadCounter;
}

class _TalkBloc extends InteractiveBloc implements TalkBloc {
_TalkBloc(this.account);
_TalkBloc(this.account) {
rooms.listen((result) {
if (!result.hasData) {
return;
}

var unread = 0;
for (final room in result.requireData) {
unread += room.unreadMessages;
}
unreadCounter.add(unread);
});

unawaited(refresh());
}

final Account account;

@override
Future<void> refresh() async {}
final rooms = BehaviorSubject();

@override
final unreadCounter = BehaviorSubject();

@override
void dispose() {
unawaited(rooms.close());
unawaited(unreadCounter.close());
super.dispose();
}

@override
Future<void> refresh() async {
await RequestManager.instance.wrapNextcloud(
account: account,
cacheKey: 'talk-rooms',
subject: rooms,
rawResponse: account.client.spreed.room.getRoomsRaw(),
unwrap: (response) => BuiltList(
response.body.ocs.data.rebuild(
(b) => b.sort((a, b) => b.lastActivity.compareTo(a.lastActivity)),
),
),
);
}
}
75 changes: 73 additions & 2 deletions packages/neon/neon_talk/lib/src/pages/main.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,81 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/utils.dart';
import 'package:neon_framework/widgets.dart';
import 'package:neon_talk/src/blocs/talk.dart';
import 'package:neon_talk/src/widgets/message_preview.dart';
import 'package:neon_talk/src/widgets/unread_indicator.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// The main page displaying the chat list.
class TalkMainPage extends StatelessWidget {
class TalkMainPage extends StatefulWidget {
/// Creates a new Talk main page.
const TalkMainPage({super.key});

@override
Widget build(BuildContext context) => const Placeholder();
State<TalkMainPage> createState() => _TalkMainPageState();
}

class _TalkMainPageState extends State<TalkMainPage> {
late String actorId;
late TalkBloc bloc;
late StreamSubscription<Object> errorsSubscription;

@override
void initState() {
super.initState();

actorId = NeonProvider.of<AccountsBloc>(context).activeAccount.value!.username;
bloc = NeonProvider.of<TalkBloc>(context);
errorsSubscription = bloc.errors.listen((error) {
NeonError.showSnackbar(context, error);
});
}

@override
void dispose() {
unawaited(errorsSubscription.cancel());
super.dispose();
}

@override
Widget build(BuildContext context) => ResultBuilder.behaviorSubject(
subject: bloc.rooms,
builder: (context, rooms) => NeonListView(
scrollKey: 'talk-rooms',
isLoading: rooms.isLoading,
error: rooms.error,
onRefresh: bloc.refresh,
itemCount: rooms.data?.length ?? 0,
itemBuilder: (context, index) => buildRoom(rooms.requireData[index]),
),
);

Widget buildRoom(spreed.Room room) {
Widget? subtitle;
Widget? trailing;

final lastChatMessage = room.lastMessage.chatMessage;
if (lastChatMessage != null) {
subtitle = TalkMessagePreview(
actorId: actorId,
roomType: spreed.RoomType.fromValue(room.type),
chatMessage: lastChatMessage,
);
}

if (room.unreadMessages > 0) {
trailing = TalkUnreadIndicator(
room: room,
);
}

return ListTile(
title: Text(room.displayName),
subtitle: subtitle,
trailing: trailing,
);
}
}
10 changes: 10 additions & 0 deletions packages/neon/neon_talk/lib/src/utils/message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// Builds a [TextSpan] for the given [chatMessage].
TextSpan buildChatMessage({
required spreed.ChatMessage chatMessage,
}) =>
TextSpan(
text: chatMessage.message,
);
56 changes: 56 additions & 0 deletions packages/neon/neon_talk/lib/src/widgets/message_preview.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:neon_talk/l10n/localizations.dart';
import 'package:neon_talk/src/utils/message.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// Displays a preview of the [chatMessage] including the display name of the sender.
class TalkMessagePreview extends StatelessWidget {
/// Creates a new Talk message preview.
const TalkMessagePreview({
required this.actorId,
required this.roomType,
required this.chatMessage,
super.key,
});

/// ID of the current actor.
final String actorId;

/// Type of the room
final spreed.RoomType roomType;

/// The chat message to preview.
final spreed.ChatMessage chatMessage;

@override
Widget build(BuildContext context) {
String? actorName;
if (chatMessage.actorId == actorId) {
actorName = TalkLocalizations.of(context).actorSelf;
} else if (!roomType.isSingleUser) {
actorName = chatMessage.actorDisplayName;
}

return RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground,
),
children: [
if (actorName != null)
TextSpan(
text: '$actorName: ',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
buildChatMessage(
chatMessage: chatMessage,
),
],
),
);
}
}
54 changes: 54 additions & 0 deletions packages/neon/neon_talk/lib/src/widgets/unread_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:nextcloud/spreed.dart' as spreed;

/// Displays the number of unread messages and whether the user was mentioned for a given [room].
class TalkUnreadIndicator extends StatelessWidget {
/// Creates a new Talk unread indicator.
const TalkUnreadIndicator({
required this.room,
super.key,
});

/// The room that the indicator will display unread messages and mentions for.
final spreed.Room room;

@override
Widget build(BuildContext context) {
assert(room.unreadMessages > 0, 'Need at least on unread message');

final colorScheme = Theme.of(context).colorScheme;

final highlight = room.unreadMention || spreed.RoomType.fromValue(room.type).isSingleUser;
final backgroundColor = highlight ? colorScheme.primaryContainer : colorScheme.background;
final textColor = highlight ? colorScheme.onPrimaryContainer : colorScheme.onBackground;

Widget? avatar;
if (room.unreadMentionDirect) {
avatar = Icon(
Icons.alternate_email,
size: 20,
color: textColor,
);
}

return Chip(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(50)),
side: BorderSide(
color: colorScheme.primaryContainer,
),
),
padding: const EdgeInsets.all(2),
backgroundColor: backgroundColor,
avatar: avatar,
label: Text(
room.unreadMessages.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
color: textColor,
),
),
);
}
}
5 changes: 5 additions & 0 deletions packages/neon/neon_talk/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ dependencies:
url: https://github.com/nextcloud/neon
path: packages/neon_framework
nextcloud: ^5.0.2
rxdart: ^0.27.0

dev_dependencies:
build_runner: ^2.4.8
flutter_test:
sdk: flutter
go_router_builder: ^2.4.1
http: ^1.2.1
mocktail: ^1.0.3
neon_lints:
git:
url: https://github.com/nextcloud/neon
Expand Down
Loading

0 comments on commit 8c19800

Please sign in to comment.