import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; class StatusMessageList extends StatelessWidget { final void Function() onStatusEdit; const StatusMessageList({ required this.onStatusEdit, super.key, }); static const double height = 108; void _onStatusTab(BuildContext context, Profile profile) { final client = Matrix.of(context).client; if (profile.userId == client.userID) return onStatusEdit(); showAdaptiveBottomSheet( context: context, builder: (c) => UserBottomSheet( profile: profile, outerContext: context, ), ); return; } @override Widget build(BuildContext context) { final client = Matrix.of(context).client; return StreamBuilder( stream: client.onSync.stream.rateLimit(const Duration(seconds: 3)), builder: (context, snapshot) { return AnimatedSize( duration: FluffyThemes.animationDuration, curve: Curves.easeInOut, child: FutureBuilder( future: Future.wait( client.interestingPresences .map((userId) => client.fetchCurrentPresence(userId)), ), builder: (context, snapshot) { final presences = snapshot.data?.where(isInterestingPresence).toList(); // If no other presences than the own entry is interesting, we // hide the presence header. if (presences == null || presences.length <= 1) { return const SizedBox.shrink(); } // Make sure own entry is at the first position. Sort by last // active instead. presences.sort((a, b) { if (a.userid == client.userID) return -1; if (b.userid == client.userID) return 1; return b.sortOrderDateTime.compareTo(a.sortOrderDateTime); }); return SizedBox( height: StatusMessageList.height, child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 8), scrollDirection: Axis.horizontal, itemCount: presences.length, itemBuilder: (context, i) => PresenceAvatar( presence: presences[i], height: StatusMessageList.height, onTap: (profile) => _onStatusTab(context, profile), ), ), ); }, ), ); }, ); } } class PresenceAvatar extends StatelessWidget { final CachedPresence presence; final double height; final void Function(Profile) onTap; const PresenceAvatar({ required this.presence, required this.height, required this.onTap, super.key, }); @override Widget build(BuildContext context) { final avatarSize = height - 16 - 16; final client = Matrix.of(context).client; return FutureBuilder( future: client.getProfileFromUserId(presence.userid), builder: (context, snapshot) { final profile = snapshot.data; final displayName = profile?.displayName ?? presence.userid.localpart ?? presence.userid; final statusMsg = presence.statusMsg; final statusMsgBubbleElevation = Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4; final statusMsgBubbleShadowColor = Theme.of(context).appBarTheme.shadowColor; final statusMsgBubbleColor = Colors.white.withAlpha(245); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: SizedBox( width: avatarSize, child: Column( children: [ HoverBuilder( builder: (context, hovered) { return AnimatedScale( scale: hovered ? 1.15 : 1.0, duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, child: InkWell( borderRadius: BorderRadius.circular(avatarSize), onTap: profile == null ? null : () => onTap(profile), child: Material( borderRadius: BorderRadius.circular(avatarSize), child: Stack( children: [ Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( gradient: presence.gradient, borderRadius: BorderRadius.circular(avatarSize), ), child: Avatar( name: displayName, mxContent: profile?.avatarUrl, size: avatarSize - 6, ), ), if (presence.userid == client.userID) Positioned( right: 0, bottom: 0, child: SizedBox( width: 24, height: 24, child: FloatingActionButton.small( heroTag: null, onPressed: () => onTap( profile ?? Profile(userId: presence.userid), ), child: const Icon( Icons.add_outlined, size: 16, ), ), ), ), if (statusMsg != null) ...[ Positioned( left: 0, top: 0, right: 8, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Material( elevation: statusMsgBubbleElevation, shadowColor: statusMsgBubbleShadowColor, borderRadius: BorderRadius.circular( AppConfig.borderRadius / 2, ), color: statusMsgBubbleColor, child: Padding( padding: const EdgeInsets.all(2.0), child: Text( statusMsg, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.black, fontSize: 10.5, ), ), ), ), Padding( padding: const EdgeInsets.only( left: 26.0, top: 4.0, ), child: Center( child: SizedBox( width: 12, height: 12, child: Material( elevation: statusMsgBubbleElevation, shadowColor: statusMsgBubbleShadowColor, borderRadius: BorderRadius.circular(99), color: statusMsgBubbleColor, ), ), ), ), ], ), ), ], ], ), ), ), ); }, ), const Spacer(), Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Text( displayName, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, ), ), ), ], ), ), ); }, ); } } extension on Client { Set get interestingPresences { final allHeroes = rooms.map((room) => room.summary.mHeroes).fold( {}, (previousValue, element) => previousValue..addAll(element ?? {}), ); allHeroes.add(userID!); return allHeroes; } } bool isInterestingPresence(CachedPresence presence) => !presence.presence.isOffline || (presence.statusMsg?.isNotEmpty ?? false); extension on CachedPresence { DateTime get sortOrderDateTime => lastActiveTimestamp ?? (currentlyActive == true ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(0)); LinearGradient get gradient => presence.isOnline == true ? LinearGradient( colors: [ Colors.green, Colors.green.shade200, Colors.green.shade900, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : presence.isUnavailable ? LinearGradient( colors: [ Colors.yellow, Colors.yellow.shade200, Colors.yellow.shade900, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : LinearGradient( colors: [ Colors.grey, Colors.grey.shade200, Colors.grey.shade900, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ); }