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

feat(neon_talk): Add room list #1657

Merged
merged 1 commit into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');

provokateurin marked this conversation as resolved.
Show resolved Hide resolved
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;
Leptopoda marked this conversation as resolved.
Show resolved Hide resolved

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