mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-10 06:25:15 +00:00
feat: Display presences in the app
This commit is contained in:
parent
a3db754b85
commit
7930150cb4
12 changed files with 187 additions and 33 deletions
|
@ -4,8 +4,10 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pages/chat/chat.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/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
|
import 'package:fluffychat/widgets/presence_builder.dart';
|
||||||
|
|
||||||
class ChatAppBarTitle extends StatelessWidget {
|
class ChatAppBarTitle extends StatelessWidget {
|
||||||
final ChatController controller;
|
final ChatController controller;
|
||||||
|
@ -34,16 +36,37 @@ class ChatAppBarTitle extends StatelessWidget {
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
),
|
),
|
||||||
size: 32,
|
size: 32,
|
||||||
|
presenceUserId: room.directChatMatrixID,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: ListTile(
|
||||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
contentPadding: EdgeInsets.zero,
|
||||||
maxLines: 1,
|
title: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||||
style: const TextStyle(
|
maxLines: 1,
|
||||||
fontSize: 16,
|
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();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -51,6 +51,7 @@ class EventInfoDialog extends StatelessWidget {
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
mxContent: event.senderFromMemoryOrFallback.avatarUrl,
|
mxContent: event.senderFromMemoryOrFallback.avatarUrl,
|
||||||
name: event.senderFromMemoryOrFallback.calcDisplayname(),
|
name: event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||||
|
presenceUserId: event.senderId,
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.sender),
|
title: Text(L10n.of(context)!.sender),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
|
|
@ -142,6 +142,7 @@ class Message extends StatelessWidget {
|
||||||
return Avatar(
|
return Avatar(
|
||||||
mxContent: user.avatarUrl,
|
mxContent: user.avatarUrl,
|
||||||
name: user.calcDisplayname(),
|
name: user.calcDisplayname(),
|
||||||
|
presenceUserId: user.stateKey,
|
||||||
onTap: () => onAvatarTab(event),
|
onTap: () => onAvatarTab(event),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -80,6 +80,7 @@ class MessageContent extends StatelessWidget {
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
mxContent: sender.avatarUrl,
|
mxContent: sender.avatarUrl,
|
||||||
name: sender.calcDisplayname(),
|
name: sender.calcDisplayname(),
|
||||||
|
presenceUserId: sender.stateKey,
|
||||||
),
|
),
|
||||||
title: Text(sender.calcDisplayname()),
|
title: Text(sender.calcDisplayname()),
|
||||||
subtitle: Text(event.originServerTs.localizedTime(context)),
|
subtitle: Text(event.originServerTs.localizedTime(context)),
|
||||||
|
|
|
@ -210,6 +210,7 @@ class _AdaptableReactorsDialog extends StatelessWidget {
|
||||||
mxContent: reactor.avatarUrl,
|
mxContent: reactor.avatarUrl,
|
||||||
name: reactor.displayName,
|
name: reactor.displayName,
|
||||||
client: client,
|
client: client,
|
||||||
|
presenceUserId: reactor.stateKey,
|
||||||
),
|
),
|
||||||
label: Text(reactor.displayName!),
|
label: Text(reactor.displayName!),
|
||||||
),
|
),
|
||||||
|
|
|
@ -43,6 +43,7 @@ class SeenByRow extends StatelessWidget {
|
||||||
name: user.calcDisplayname(),
|
name: user.calcDisplayname(),
|
||||||
size: 16,
|
size: 16,
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
|
presenceUserId: user.stateKey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (seenByUsers.length > maxAvatars)
|
if (seenByUsers.length > maxAvatars)
|
||||||
|
|
|
@ -81,8 +81,11 @@ class ParticipantListItem extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Text(user.id),
|
subtitle: Text(user.id),
|
||||||
leading:
|
leading: Avatar(
|
||||||
Avatar(mxContent: user.avatarUrl, name: user.calcDisplayname()),
|
mxContent: user.avatarUrl,
|
||||||
|
name: user.calcDisplayname(),
|
||||||
|
presenceUserId: user.stateKey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,6 +158,11 @@ class ChatListItem extends StatelessWidget {
|
||||||
: 14.0
|
: 14.0
|
||||||
: 0.0;
|
: 0.0;
|
||||||
final hasNotifications = room.notificationCount > 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(
|
final displayname = room.getLocalizedDisplayname(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
);
|
);
|
||||||
|
@ -169,11 +174,7 @@ class ChatListItem extends StatelessWidget {
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
color: selected
|
color: backgroundColor,
|
||||||
? Theme.of(context).colorScheme.primaryContainer
|
|
||||||
: activeChat
|
|
||||||
? Theme.of(context).colorScheme.secondaryContainer
|
|
||||||
: Colors.transparent,
|
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
visualDensity: const VisualDensity(vertical: -0.5),
|
visualDensity: const VisualDensity(vertical: -0.5),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
@ -192,6 +193,8 @@ class ChatListItem extends StatelessWidget {
|
||||||
mxContent: room.avatar,
|
mxContent: room.avatar,
|
||||||
name: displayname,
|
name: displayname,
|
||||||
onTap: onLongPress,
|
onTap: onLongPress,
|
||||||
|
presenceUserId: room.directChatMatrixID,
|
||||||
|
presenceBackgroundColor: backgroundColor,
|
||||||
),
|
),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|
|
@ -157,6 +157,7 @@ class _InviteContactListTile extends StatelessWidget {
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
mxContent: avatarUrl,
|
mxContent: avatarUrl,
|
||||||
name: displayname,
|
name: displayname,
|
||||||
|
presenceUserId: userId,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
displayname,
|
displayname,
|
||||||
|
|
|
@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
|
import 'package:fluffychat/widgets/presence_builder.dart';
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
import 'user_bottom_sheet.dart';
|
import 'user_bottom_sheet.dart';
|
||||||
|
|
||||||
|
@ -30,7 +32,47 @@ class UserBottomSheetView extends StatelessWidget {
|
||||||
leading: CloseButton(
|
leading: CloseButton(
|
||||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
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: [
|
actions: [
|
||||||
if (userId != client.userID &&
|
if (userId != client.userID &&
|
||||||
!client.ignoredUsers.contains(userId))
|
!client.ignoredUsers.contains(userId))
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/utils/string_color.dart';
|
import 'package:fluffychat/utils/string_color.dart';
|
||||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
|
import 'package:fluffychat/widgets/presence_builder.dart';
|
||||||
|
|
||||||
class Avatar extends StatelessWidget {
|
class Avatar extends StatelessWidget {
|
||||||
final Uri? mxContent;
|
final Uri? mxContent;
|
||||||
|
@ -13,6 +14,8 @@ class Avatar extends StatelessWidget {
|
||||||
static const double defaultSize = 44;
|
static const double defaultSize = 44;
|
||||||
final Client? client;
|
final Client? client;
|
||||||
final double fontSize;
|
final double fontSize;
|
||||||
|
final String? presenceUserId;
|
||||||
|
final Color? presenceBackgroundColor;
|
||||||
|
|
||||||
const Avatar({
|
const Avatar({
|
||||||
this.mxContent,
|
this.mxContent,
|
||||||
|
@ -21,6 +24,8 @@ class Avatar extends StatelessWidget {
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.client,
|
this.client,
|
||||||
this.fontSize = 18,
|
this.fontSize = 18,
|
||||||
|
this.presenceUserId,
|
||||||
|
this.presenceBackgroundColor,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -48,26 +53,64 @@ class Avatar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final borderRadius = BorderRadius.circular(size / 2);
|
final borderRadius = BorderRadius.circular(size / 2);
|
||||||
final container = ClipRRect(
|
final presenceUserId = this.presenceUserId;
|
||||||
borderRadius: borderRadius,
|
final color =
|
||||||
child: Container(
|
noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor;
|
||||||
width: size,
|
final container = Stack(
|
||||||
height: size,
|
children: [
|
||||||
color: noPic
|
ClipRRect(
|
||||||
? name?.lightColorAvatar
|
borderRadius: borderRadius,
|
||||||
: Theme.of(context).secondaryHeaderColor,
|
child: Container(
|
||||||
child: noPic
|
width: size,
|
||||||
? textWidget
|
height: size,
|
||||||
: MxcImage(
|
color: color,
|
||||||
key: Key(mxContent.toString()),
|
child: noPic
|
||||||
uri: mxContent,
|
? textWidget
|
||||||
fit: BoxFit.cover,
|
: MxcImage(
|
||||||
width: size,
|
key: Key(mxContent.toString()),
|
||||||
height: size,
|
uri: mxContent,
|
||||||
placeholder: (_) => textWidget,
|
fit: BoxFit.cover,
|
||||||
cacheKey: mxContent.toString(),
|
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;
|
if (onTap == null) return container;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
|
34
lib/widgets/presence_builder.dart
Normal file
34
lib/widgets/presence_builder.dart
Normal 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],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue