feat: Display typing indicators with gif

This commit is contained in:
Krille Fear 2021-11-14 09:36:35 +01:00
parent 941d28a81d
commit 14fe60d8e0
4 changed files with 235 additions and 135 deletions

BIN
assets/typing.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/room_status_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ChatAppBarTitle extends StatelessWidget {
final ChatController controller;
const ChatAppBarTitle(this.controller, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (controller.selectedEvents.isNotEmpty) {
return Text(controller.selectedEvents.length.toString());
}
return ListTile(
leading: Avatar(controller.room.avatar, controller.room.displayname),
contentPadding: EdgeInsets.zero,
onTap: controller.room.isDirectChat
? () => showModalBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: controller.room
.getUserByMXIDSync(controller.room.directChatMatrixID),
outerContext: context,
onMention: () => controller.sendController.text +=
'${controller.room.getUserByMXIDSync(controller.room.directChatMatrixID).mention} ',
),
)
: () => VRouter.of(context)
.toSegments(['rooms', controller.room.id, 'details']),
title: Text(
controller.room
.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
maxLines: 1),
subtitle: StreamBuilder<Object>(
stream: Matrix.of(context)
.client
.onPresence
.stream
.where((p) => p.senderId == controller.room.directChatMatrixID)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) => Text(
controller.room.getLocalizedStatus(context),
maxLines: 1,
//overflow: TextOverflow.ellipsis,
),
),
);
}
}

View File

@ -13,15 +13,14 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/pages/chat/seen_by_row.dart';
import 'package:fluffychat/pages/chat/tombstone_display.dart';
import 'package:fluffychat/pages/chat/typing_indicators.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/room_status_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -38,120 +37,8 @@ class ChatView extends StatelessWidget {
const ChatView(this.controller, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
controller.matrix ??= Matrix.of(context);
final client = controller.matrix.client;
controller.sendingClient ??= client;
controller.room = controller.sendingClient.getRoomById(controller.roomId);
if (controller.room == null) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
),
);
}
if (controller.room.membership == Membership.invite) {
showFutureLoadingDialog(
context: context, future: () => controller.room.join());
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return VWidgetGuard(
onSystemPop: (redirector) async {
if (controller.selectedEvents.isNotEmpty) {
controller.clearSelectedEvents();
redirector.stopRedirection();
}
},
child: StreamBuilder(
stream: controller.room.onUpdate.stream
.rateLimit(const Duration(milliseconds: 250)),
builder: (context, snapshot) => Scaffold(
appBar: AppBar(
actionsIconTheme: IconThemeData(
color: controller.selectedEvents.isEmpty
? null
: Theme.of(context).colorScheme.primary,
),
leading: controller.selectMode
? IconButton(
icon: const Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
tooltip: L10n.of(context).close,
color: Theme.of(context).colorScheme.primary,
)
: UnreadBadgeBackButton(roomId: controller.roomId),
titleSpacing: 0,
title: controller.selectedEvents.isEmpty
? ListTile(
leading: Avatar(
controller.room.avatar, controller.room.displayname),
contentPadding: EdgeInsets.zero,
onTap: controller.room.isDirectChat
? () => showModalBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: controller.room.getUserByMXIDSync(
controller.room.directChatMatrixID),
outerContext: context,
onMention: () => controller
.sendController.text +=
'${controller.room.getUserByMXIDSync(controller.room.directChatMatrixID).mention} ',
),
)
: () => VRouter.of(context).toSegments(
['rooms', controller.room.id, 'details']),
title: Text(
controller.room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context))),
maxLines: 1),
subtitle: controller.room
.getLocalizedTypingText(context)
.isEmpty
? StreamBuilder<Object>(
stream: Matrix.of(context)
.client
.onPresence
.stream
.where((p) =>
p.senderId ==
controller.room.directChatMatrixID)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) => Text(
controller.room.getLocalizedStatus(context),
maxLines: 1,
//overflow: TextOverflow.ellipsis,
))
: Row(
children: <Widget>[
Icon(Icons.edit_outlined,
color:
Theme.of(context).colorScheme.secondary,
size: 13),
const SizedBox(width: 4),
Expanded(
child: Text(
controller.room
.getLocalizedTypingText(context),
maxLines: 1,
style: TextStyle(
color:
Theme.of(context).colorScheme.secondary,
fontStyle: FontStyle.italic,
),
),
),
],
),
)
: Text(controller.selectedEvents.length.toString()),
actions: controller.selectMode
? <Widget>[
List<Widget> _appBarActions(BuildContext context) => controller.selectMode
? [
if (controller.canEditSelectedEvents)
IconButton(
icon: const Icon(Icons.edit_outlined),
@ -208,16 +95,67 @@ class ChatView extends StatelessWidget {
],
),
]
: <Widget>[
: [
if (controller.room.canSendDefaultStates)
IconButton(
tooltip: L10n.of(context).videoCall,
icon: const Icon(Icons.video_call_outlined),
onPressed: controller.startCallAction,
),
ChatSettingsPopupMenu(
controller.room, !controller.room.isDirectChat),
],
ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat),
];
@override
Widget build(BuildContext context) {
controller.matrix ??= Matrix.of(context);
final client = controller.matrix.client;
controller.sendingClient ??= client;
controller.room = controller.sendingClient.getRoomById(controller.roomId);
if (controller.room == null) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
),
);
}
if (controller.room.membership == Membership.invite) {
showFutureLoadingDialog(
context: context, future: () => controller.room.join());
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return VWidgetGuard(
onSystemPop: (redirector) async {
if (controller.selectedEvents.isNotEmpty) {
controller.clearSelectedEvents();
redirector.stopRedirection();
}
},
child: StreamBuilder(
stream: controller.room.onUpdate.stream
.rateLimit(const Duration(milliseconds: 250)),
builder: (context, snapshot) => Scaffold(
appBar: AppBar(
actionsIconTheme: IconThemeData(
color: controller.selectedEvents.isEmpty
? null
: Theme.of(context).colorScheme.primary,
),
leading: controller.selectMode
? IconButton(
icon: const Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
tooltip: L10n.of(context).close,
color: Theme.of(context).colorScheme.primary,
)
: UnreadBadgeBackButton(roomId: controller.roomId),
titleSpacing: 0,
title: ChatAppBarTitle(controller),
actions: _appBarActions(context),
),
floatingActionButton: controller.showScrollDownButton
? Padding(
@ -300,7 +238,13 @@ class ChatView extends StatelessWidget {
)
: Container()
: i == 0
? SeenByRow(controller)
? Column(
mainAxisSize: MainAxisSize.min,
children: [
SeenByRow(controller),
TypingIndicators(controller),
],
)
: AutoScrollTag(
key: ValueKey(controller
.filteredEvents[i - 1].eventId),

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
class TypingIndicators extends StatelessWidget {
final ChatController controller;
const TypingIndicators(this.controller, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final typingUsers = controller.room.typingUsers
..removeWhere((u) => u.stateKey == Matrix.of(context).client.userID);
const topPadding = 24.0;
const bottomPadding = 4.0;
return Container(
width: double.infinity,
alignment: Alignment.center,
child: AnimatedContainer(
constraints:
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
height: typingUsers.isEmpty
? 0
: Avatar.defaultSize + bottomPadding + topPadding,
duration: const Duration(milliseconds: 300),
curve: Curves.bounceInOut,
alignment: controller.filteredEvents.isNotEmpty &&
controller.filteredEvents.first.senderId ==
Matrix.of(context).client.userID
? Alignment.topRight
: Alignment.topLeft,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
padding: EdgeInsets.only(
left: typingUsers.length < 2 ? 8 : 0,
bottom: bottomPadding,
),
child: Row(
children: [
SizedBox(
height: Avatar.defaultSize,
width: typingUsers.length < 2
? Avatar.defaultSize
: Avatar.defaultSize + 8,
child: Stack(
children: [
if (typingUsers.isNotEmpty)
Avatar(
typingUsers.first.avatarUrl,
typingUsers.first.calcDisplayname(),
),
if (typingUsers.length == 2)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Avatar(
typingUsers.length == 2
? typingUsers.last.avatarUrl
: null,
typingUsers.length == 2
? typingUsers.last.calcDisplayname()
: '+${typingUsers.length - 1}',
),
),
],
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(top: topPadding),
child: Material(
color: Theme.of(context).backgroundColor,
elevation: 6,
shadowColor:
Theme.of(context).secondaryHeaderColor.withAlpha(100),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(2),
topRight: Radius.circular(AppConfig.borderRadius),
bottomLeft: Radius.circular(AppConfig.borderRadius),
bottomRight: Radius.circular(AppConfig.borderRadius),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: typingUsers.isEmpty
? null
: Image.asset('assets/typing.gif', height: 12),
),
),
),
],
),
),
);
}
}