165 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
| import 'dart:async';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| 
 | |
| import 'package:extera_next/config/app_config.dart';
 | |
| import 'package:extera_next/config/themes.dart';
 | |
| import 'package:extera_next/pages/chat/chat.dart';
 | |
| import 'package:extera_next/widgets/avatar.dart';
 | |
| import 'package:extera_next/widgets/matrix.dart';
 | |
| 
 | |
| class TypingIndicators extends StatelessWidget {
 | |
|   final ChatController controller;
 | |
|   const TypingIndicators(this.controller, {super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final theme = Theme.of(context);
 | |
| 
 | |
|     const avatarSize = Avatar.defaultSize / 2;
 | |
| 
 | |
|     return StreamBuilder<Object>(
 | |
|       stream: controller.room.client.onSync.stream.where(
 | |
|         (syncUpdate) =>
 | |
|             syncUpdate.rooms?.join?[controller.room.id]?.ephemeral
 | |
|                 ?.any((ephemeral) => ephemeral.type == 'm.typing') ??
 | |
|             false,
 | |
|       ),
 | |
|       builder: (context, _) {
 | |
|         final typingUsers = controller.room.typingUsers
 | |
|           ..removeWhere((u) => u.stateKey == Matrix.of(context).client.userID);
 | |
| 
 | |
|         return Container(
 | |
|           width: double.infinity,
 | |
|           alignment: Alignment.center,
 | |
|           child: AnimatedContainer(
 | |
|             constraints:
 | |
|                 const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
 | |
|             height: typingUsers.isEmpty ? 0 : avatarSize + 8,
 | |
|             duration: FluffyThemes.animationDuration,
 | |
|             curve: FluffyThemes.animationCurve,
 | |
|             alignment: controller.timeline!.events.isNotEmpty &&
 | |
|                     controller.timeline!.events.first.senderId ==
 | |
|                         Matrix.of(context).client.userID
 | |
|                 ? Alignment.topRight
 | |
|                 : Alignment.topLeft,
 | |
|             clipBehavior: Clip.hardEdge,
 | |
|             decoration: const BoxDecoration(),
 | |
|             padding: const EdgeInsets.symmetric(
 | |
|               horizontal: 8.0,
 | |
|               vertical: 4.0,
 | |
|             ),
 | |
|             child: Row(
 | |
|               children: [
 | |
|                 Container(
 | |
|                   alignment: Alignment.center,
 | |
|                   height: avatarSize,
 | |
|                   width: Avatar.defaultSize,
 | |
|                   child: Stack(
 | |
|                     children: [
 | |
|                       if (typingUsers.isNotEmpty)
 | |
|                         Avatar(
 | |
|                           size: avatarSize,
 | |
|                           mxContent: typingUsers.first.avatarUrl,
 | |
|                           name: typingUsers.first.calcDisplayname(),
 | |
|                         ),
 | |
|                       if (typingUsers.length == 2)
 | |
|                         Padding(
 | |
|                           padding: const EdgeInsets.only(left: 16),
 | |
|                           child: Avatar(
 | |
|                             size: avatarSize,
 | |
|                             mxContent: typingUsers.length == 2
 | |
|                                 ? typingUsers.last.avatarUrl
 | |
|                                 : null,
 | |
|                             name: typingUsers.length == 2
 | |
|                                 ? typingUsers.last.calcDisplayname()
 | |
|                                 : '+${typingUsers.length - 1}',
 | |
|                           ),
 | |
|                         ),
 | |
|                     ],
 | |
|                   ),
 | |
|                 ),
 | |
|                 const SizedBox(width: 8),
 | |
|                 Material(
 | |
|                   color: theme.colorScheme.surfaceContainerHigh,
 | |
|                   borderRadius: const BorderRadius.all(
 | |
|                     Radius.circular(AppConfig.borderRadius),
 | |
|                   ),
 | |
|                   child: Padding(
 | |
|                     padding: const EdgeInsets.symmetric(horizontal: 8),
 | |
|                     child: typingUsers.isEmpty ? null : const _TypingDots(),
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _TypingDots extends StatefulWidget {
 | |
|   const _TypingDots();
 | |
| 
 | |
|   @override
 | |
|   State<_TypingDots> createState() => __TypingDotsState();
 | |
| }
 | |
| 
 | |
| class __TypingDotsState extends State<_TypingDots> {
 | |
|   int _tick = 0;
 | |
| 
 | |
|   late final Timer _timer;
 | |
| 
 | |
|   static const Duration animationDuration = Duration(milliseconds: 300);
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     _timer = Timer.periodic(
 | |
|       animationDuration,
 | |
|       (_) {
 | |
|         if (!mounted) {
 | |
|           return;
 | |
|         }
 | |
|         setState(() {
 | |
|           _tick = (_tick + 1) % 4;
 | |
|         });
 | |
|       },
 | |
|     );
 | |
|     super.initState();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _timer.cancel();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final theme = Theme.of(context);
 | |
|     const size = 8.0;
 | |
| 
 | |
|     return Row(
 | |
|       mainAxisSize: MainAxisSize.min,
 | |
|       children: [
 | |
|         for (var i = 1; i <= 3; i++)
 | |
|           AnimatedContainer(
 | |
|             duration: animationDuration * 1.5,
 | |
|             curve: FluffyThemes.animationCurve,
 | |
|             width: size,
 | |
|             height: _tick == i ? size * 2 : size,
 | |
|             margin: EdgeInsets.symmetric(
 | |
|               horizontal: 2,
 | |
|               vertical: _tick == i ? 4 : 8,
 | |
|             ),
 | |
|             decoration: BoxDecoration(
 | |
|               borderRadius: BorderRadius.circular(size * 2),
 | |
|               color: theme.colorScheme.secondary,
 | |
|             ),
 | |
|           ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 |