feat: Display presences in the app

This commit is contained in:
krille-chan 2023-11-11 17:56:23 +01:00
parent a3db754b85
commit 7930150cb4
No known key found for this signature in database
12 changed files with 187 additions and 33 deletions

View file

@ -4,8 +4,10 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
class ChatAppBarTitle extends StatelessWidget {
final ChatController controller;
@ -34,16 +36,37 @@ class ChatAppBarTitle extends StatelessWidget {
MatrixLocals(L10n.of(context)!),
),
size: 32,
presenceUserId: room.directChatMatrixID,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
),
),
subtitle: PresenceBuilder(
userId: room.directChatMatrixID,
builder: (context, presence) {
final lastActiveTimestamp = presence?.lastActiveTimestamp;
if (presence?.currentlyActive == true) {
return Text(L10n.of(context)!.currentlyActive);
}
if (lastActiveTimestamp != null) {
return Text(
L10n.of(context)!.lastActiveAgo(
lastActiveTimestamp.localizedTimeShort(context),
),
);
}
return const SizedBox.shrink();
},
),
),
),

View file

@ -51,6 +51,7 @@ class EventInfoDialog extends StatelessWidget {
leading: Avatar(
mxContent: event.senderFromMemoryOrFallback.avatarUrl,
name: event.senderFromMemoryOrFallback.calcDisplayname(),
presenceUserId: event.senderId,
),
title: Text(L10n.of(context)!.sender),
subtitle: Text(

View file

@ -142,6 +142,7 @@ class Message extends StatelessWidget {
return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
onTap: () => onAvatarTab(event),
);
},

View file

@ -80,6 +80,7 @@ class MessageContent extends StatelessWidget {
leading: Avatar(
mxContent: sender.avatarUrl,
name: sender.calcDisplayname(),
presenceUserId: sender.stateKey,
),
title: Text(sender.calcDisplayname()),
subtitle: Text(event.originServerTs.localizedTime(context)),

View file

@ -210,6 +210,7 @@ class _AdaptableReactorsDialog extends StatelessWidget {
mxContent: reactor.avatarUrl,
name: reactor.displayName,
client: client,
presenceUserId: reactor.stateKey,
),
label: Text(reactor.displayName!),
),

View file

@ -43,6 +43,7 @@ class SeenByRow extends StatelessWidget {
name: user.calcDisplayname(),
size: 16,
fontSize: 9,
presenceUserId: user.stateKey,
),
),
if (seenByUsers.length > maxAvatars)

View file

@ -81,8 +81,11 @@ class ParticipantListItem extends StatelessWidget {
],
),
subtitle: Text(user.id),
leading:
Avatar(mxContent: user.avatarUrl, name: user.calcDisplayname()),
leading: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
),
),
);
}

View file

@ -158,6 +158,11 @@ class ChatListItem extends StatelessWidget {
: 14.0
: 0.0;
final hasNotifications = room.notificationCount > 0;
final backgroundColor = selected
? Theme.of(context).colorScheme.primaryContainer
: activeChat
? Theme.of(context).colorScheme.secondaryContainer
: null;
final displayname = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
@ -169,11 +174,7 @@ class ChatListItem extends StatelessWidget {
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
color: selected
? Theme.of(context).colorScheme.primaryContainer
: activeChat
? Theme.of(context).colorScheme.secondaryContainer
: Colors.transparent,
color: backgroundColor,
child: ListTile(
visualDensity: const VisualDensity(vertical: -0.5),
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
@ -192,6 +193,8 @@ class ChatListItem extends StatelessWidget {
mxContent: room.avatar,
name: displayname,
onTap: onLongPress,
presenceUserId: room.directChatMatrixID,
presenceBackgroundColor: backgroundColor,
),
title: Row(
children: <Widget>[

View file

@ -157,6 +157,7 @@ class _InviteContactListTile extends StatelessWidget {
leading: Avatar(
mxContent: avatarUrl,
name: displayname,
presenceUserId: userId,
),
title: Text(
displayname,

View file

@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
import '../../widgets/matrix.dart';
import 'user_bottom_sheet.dart';
@ -30,7 +32,47 @@ class UserBottomSheetView extends StatelessWidget {
leading: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
title: Text(displayname.trim().split(' ').first),
title: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(displayname.trim().split(' ').first),
subtitle: PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
if (presence == null) return const SizedBox.shrink();
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.red;
final lastActiveTimestamp = presence.lastActiveTimestamp;
return Row(
children: [
Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
),
),
if (presence.currentlyActive == true)
Text(L10n.of(context)!.currentlyActive),
if (lastActiveTimestamp != null)
Text(
L10n.of(context)!.lastActiveAgo(
lastActiveTimestamp.localizedTimeShort(context),
),
),
],
);
},
),
),
actions: [
if (userId != client.userID &&
!client.ignoredUsers.contains(userId))

View file

@ -4,6 +4,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
class Avatar extends StatelessWidget {
final Uri? mxContent;
@ -13,6 +14,8 @@ class Avatar extends StatelessWidget {
static const double defaultSize = 44;
final Client? client;
final double fontSize;
final String? presenceUserId;
final Color? presenceBackgroundColor;
const Avatar({
this.mxContent,
@ -21,6 +24,8 @@ class Avatar extends StatelessWidget {
this.onTap,
this.client,
this.fontSize = 18,
this.presenceUserId,
this.presenceBackgroundColor,
super.key,
});
@ -48,26 +53,64 @@ class Avatar extends StatelessWidget {
),
);
final borderRadius = BorderRadius.circular(size / 2);
final container = ClipRRect(
borderRadius: borderRadius,
child: Container(
width: size,
height: size,
color: noPic
? name?.lightColorAvatar
: Theme.of(context).secondaryHeaderColor,
child: noPic
? textWidget
: MxcImage(
key: Key(mxContent.toString()),
uri: mxContent,
fit: BoxFit.cover,
width: size,
height: size,
placeholder: (_) => textWidget,
cacheKey: mxContent.toString(),
final presenceUserId = this.presenceUserId;
final color =
noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor;
final container = Stack(
children: [
ClipRRect(
borderRadius: borderRadius,
child: Container(
width: size,
height: size,
color: color,
child: noPic
? textWidget
: MxcImage(
key: Key(mxContent.toString()),
uri: mxContent,
fit: BoxFit.cover,
width: size,
height: size,
placeholder: (_) => textWidget,
cacheKey: mxContent.toString(),
),
),
),
PresenceBuilder(
userId: presenceUserId,
builder: (context, presence) {
if (presence == null) return const SizedBox.shrink();
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.red;
return Positioned(
bottom: -4,
right: -4,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: presenceBackgroundColor ??
Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(32),
),
alignment: Alignment.center,
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
),
),
),
),
);
},
),
],
);
if (onTap == null) return container;
return InkWell(

View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PresenceBuilder extends StatelessWidget {
final Widget Function(BuildContext context, CachedPresence? presence) builder;
final String? userId;
final Client? client;
const PresenceBuilder({
required this.builder,
this.userId,
this.client,
super.key,
});
@override
Widget build(BuildContext context) {
final userId = this.userId;
if (userId == null) return builder(context, null);
final client = this.client ?? Matrix.of(context).client;
return StreamBuilder(
stream: client.onPresenceChanged.stream
.where((cachedPresence) => cachedPresence.userid == userId),
builder: (context, snapshot) => builder(
context,
snapshot.data ?? client.presences[userId],
),
);
}
}