refactor: Replace stories feature with presence status msg
This commit is contained in:
parent
9c24547b7f
commit
895de76e70
21 changed files with 364 additions and 2386 deletions
|
|
@ -18,7 +18,6 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import '../../../utils/account_bundles.dart';
|
||||
|
|
@ -139,13 +138,11 @@ class ChatListController extends State<ChatList>
|
|||
bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) {
|
||||
switch (activeFilter) {
|
||||
case ActiveFilter.allChats:
|
||||
return (room) => !room.isSpace && !room.isStoryRoom;
|
||||
return (room) => !room.isSpace;
|
||||
case ActiveFilter.groups:
|
||||
return (room) =>
|
||||
!room.isSpace && !room.isDirectChat && !room.isStoryRoom;
|
||||
return (room) => !room.isSpace && !room.isDirectChat;
|
||||
case ActiveFilter.messages:
|
||||
return (room) =>
|
||||
!room.isSpace && room.isDirectChat && !room.isStoryRoom;
|
||||
return (room) => !room.isSpace && room.isDirectChat;
|
||||
case ActiveFilter.spaces:
|
||||
return (r) => r.isSpace;
|
||||
}
|
||||
|
|
@ -487,11 +484,15 @@ class ChatListController extends State<ChatList>
|
|||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.setStatus,
|
||||
message: L10n.of(context)!.leaveEmptyToClearStatus,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context)!.statusExampleMessage,
|
||||
maxLines: 6,
|
||||
minLines: 1,
|
||||
maxLength: 255,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
import 'package:fluffychat/pages/chat_list/stories_header.dart';
|
||||
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -71,11 +70,6 @@ class ChatListViewBody extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
final rooms = controller.filteredRooms;
|
||||
final displayStoriesHeader = {
|
||||
ActiveFilter.allChats,
|
||||
ActiveFilter.messages,
|
||||
}.contains(controller.activeFilter) &&
|
||||
client.storiesRooms.isNotEmpty;
|
||||
return SafeArea(
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
|
|
@ -158,16 +152,10 @@ class ChatListViewBody extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
SearchTitle(
|
||||
title: L10n.of(context)!.stories,
|
||||
icon: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
],
|
||||
if (displayStoriesHeader)
|
||||
StoriesHeader(
|
||||
key: const Key('stories_header'),
|
||||
filter: controller.searchController.text,
|
||||
),
|
||||
StatusMessageList(
|
||||
onStatusEdit: controller.setStatus,
|
||||
),
|
||||
const ConnectionStatusHeader(),
|
||||
AnimatedContainer(
|
||||
height: controller.isTorBrowser ? 64 : 0,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
toolbarHeight: Theme.of(context).appBarTheme.toolbarHeight ?? 56,
|
||||
toolbarHeight: (Theme.of(context).appBarTheme.toolbarHeight ?? 56) + 16,
|
||||
pinned:
|
||||
FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal,
|
||||
scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null,
|
||||
|
|
@ -55,19 +55,27 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintText: L10n.of(context)!.searchChatsRooms,
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
prefixIcon: controller.isSearchMode
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelSearch,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: controller.startSearch,
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
suffixIcon: controller.isSearchMode
|
||||
|
|
|
|||
|
|
@ -28,16 +28,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
: 1,
|
||||
);
|
||||
return <PopupMenuEntry<Object>>[
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.newStory,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.camera_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context)!.yourStory),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.newGroup,
|
||||
child: Row(
|
||||
|
|
@ -58,6 +48,16 @@ class ClientChooserButton extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.setStatus,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context)!.setStatus),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.invite,
|
||||
child: Row(
|
||||
|
|
@ -260,9 +260,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
if (consent != OkCancelResult.ok) return;
|
||||
context.go('/rooms/settings/addaccount');
|
||||
break;
|
||||
case SettingsAction.newStory:
|
||||
context.go('/rooms/stories/create');
|
||||
break;
|
||||
case SettingsAction.newGroup:
|
||||
context.go('/rooms/newgroup');
|
||||
break;
|
||||
|
|
@ -278,6 +275,9 @@ class ClientChooserButton extends StatelessWidget {
|
|||
case SettingsAction.archive:
|
||||
context.go('/rooms/archive');
|
||||
break;
|
||||
case SettingsAction.setStatus:
|
||||
controller.setStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -354,9 +354,9 @@ class ClientChooserButton extends StatelessWidget {
|
|||
|
||||
enum SettingsAction {
|
||||
addAccount,
|
||||
newStory,
|
||||
newGroup,
|
||||
newSpace,
|
||||
setStatus,
|
||||
invite,
|
||||
settings,
|
||||
archive,
|
||||
|
|
|
|||
305
lib/pages/chat_list/status_msg_list.dart
Normal file
305
lib/pages/chat_list/status_msg_list.dart
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
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<Profile>(
|
||||
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<String> get interestingPresences {
|
||||
final allHeroes = rooms.map((room) => room.summary.mHeroes).fold(
|
||||
<String>{},
|
||||
(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,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../config/themes.dart';
|
||||
|
||||
enum ContextualRoomAction {
|
||||
mute,
|
||||
unmute,
|
||||
leave,
|
||||
}
|
||||
|
||||
class StoriesHeader extends StatelessWidget {
|
||||
final String filter;
|
||||
|
||||
const StoriesHeader({required this.filter, super.key});
|
||||
|
||||
void _addToStoryAction(BuildContext context) =>
|
||||
context.go('/rooms/stories/create');
|
||||
|
||||
void _goToStoryAction(BuildContext context, String roomId) async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId);
|
||||
if (room == null) return;
|
||||
if (room.membership != Membership.join) {
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.join,
|
||||
);
|
||||
if (result.error != null) return;
|
||||
}
|
||||
context.go('/rooms/stories/$roomId');
|
||||
}
|
||||
|
||||
void _contextualActions(BuildContext context, Room room) async {
|
||||
final action = await showModalActionSheet<ContextualRoomAction>(
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
context: context,
|
||||
actions: [
|
||||
if (room.pushRuleState != PushRuleState.notify)
|
||||
SheetAction(
|
||||
label: L10n.of(context)!.unmuteChat,
|
||||
key: ContextualRoomAction.unmute,
|
||||
icon: Icons.notifications_outlined,
|
||||
)
|
||||
else
|
||||
SheetAction(
|
||||
label: L10n.of(context)!.muteChat,
|
||||
key: ContextualRoomAction.mute,
|
||||
icon: Icons.notifications_off_outlined,
|
||||
),
|
||||
SheetAction(
|
||||
label: L10n.of(context)!.unsubscribeStories,
|
||||
key: ContextualRoomAction.leave,
|
||||
icon: Icons.unsubscribe_outlined,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (action == null) return;
|
||||
switch (action) {
|
||||
case ContextualRoomAction.mute:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(PushRuleState.dontNotify),
|
||||
);
|
||||
break;
|
||||
case ContextualRoomAction.unmute:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(PushRuleState.notify),
|
||||
);
|
||||
break;
|
||||
case ContextualRoomAction.leave:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.leave(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
if (Matrix.of(context).shareContent != null) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
radius: Avatar.defaultSize / 2,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
child: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)!.addToStory),
|
||||
onTap: () => _addToStoryAction(context),
|
||||
);
|
||||
}
|
||||
final ownStoryRoom = client.storiesRooms
|
||||
.firstWhereOrNull((r) => r.creatorId == client.userID);
|
||||
final stories = [
|
||||
if (ownStoryRoom != null) ownStoryRoom,
|
||||
...client.storiesRooms..remove(ownStoryRoom),
|
||||
];
|
||||
return SizedBox(
|
||||
height: 104,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: stories.length,
|
||||
itemBuilder: (context, i) {
|
||||
final room = stories[i];
|
||||
final creator = room
|
||||
.unsafeGetUserFromMemoryOrFallback(room.creatorId ?? 'Unknown');
|
||||
final userId = room.creatorId;
|
||||
final displayname = creator.calcDisplayname();
|
||||
final avatarUrl = creator.avatarUrl;
|
||||
if (!displayname.toLowerCase().contains(filter.toLowerCase())) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _StoryButton(
|
||||
profile: Profile(
|
||||
displayName: displayname,
|
||||
avatarUrl: avatarUrl,
|
||||
userId: userId ?? 'Unknown',
|
||||
),
|
||||
lastMessage: room.hasPosts
|
||||
? room.lastEvent?.calcLocalizedBodyFallback(
|
||||
MatrixLocals(
|
||||
L10n.of(context)!,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
heroTag: 'stories_${room.id}',
|
||||
hasPosts: room.hasPosts || room == ownStoryRoom,
|
||||
showEditFab: userId == client.userID,
|
||||
unread: room.membership == Membership.invite ||
|
||||
(room.hasNewMessages && room.hasPosts),
|
||||
onPressed: () => _goToStoryAction(context, room.id),
|
||||
onLongPressed: () => _contextualActions(context, room),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Room {
|
||||
bool get hasPosts {
|
||||
if (membership == Membership.invite) return true;
|
||||
final lastEvent = this.lastEvent;
|
||||
if (lastEvent == null) return false;
|
||||
if (lastEvent.type != EventTypes.Message) return false;
|
||||
if (DateTime.now().difference(lastEvent.originServerTs).inHours >
|
||||
ClientStoriesExtension.lifeTimeInHours) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _StoryButton extends StatefulWidget {
|
||||
final Profile profile;
|
||||
final bool showEditFab;
|
||||
final bool unread;
|
||||
final bool hasPosts;
|
||||
final void Function() onPressed;
|
||||
final void Function()? onLongPressed;
|
||||
final String heroTag;
|
||||
final String? lastMessage;
|
||||
|
||||
const _StoryButton({
|
||||
required this.profile,
|
||||
required this.onPressed,
|
||||
required this.heroTag,
|
||||
required this.lastMessage,
|
||||
this.showEditFab = false,
|
||||
this.hasPosts = true,
|
||||
this.unread = false,
|
||||
this.onLongPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_StoryButton> createState() => _StoryButtonState();
|
||||
}
|
||||
|
||||
class _StoryButtonState extends State<_StoryButton> {
|
||||
bool _hovered = false;
|
||||
|
||||
void _onHover(bool hover) {
|
||||
if (hover == _hovered) return;
|
||||
setState(() {
|
||||
_hovered = hover;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final lastMessage = widget.lastMessage;
|
||||
final lastMessageBubbleElevation =
|
||||
Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4;
|
||||
final lastMessageBubbleShadowColor =
|
||||
Theme.of(context).appBarTheme.shadowColor;
|
||||
final lastMessageBubbleColor = Colors.white.withAlpha(245);
|
||||
return SizedBox(
|
||||
width: 82,
|
||||
child: InkWell(
|
||||
onHover: _onHover,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPressed,
|
||||
child: Opacity(
|
||||
opacity: widget.hasPosts ? 1 : 0.4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
AnimatedScale(
|
||||
scale: _hovered ? 1.15 : 1.0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(Avatar.defaultSize),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
gradient: widget.unread
|
||||
? const LinearGradient(
|
||||
colors: [
|
||||
Colors.red,
|
||||
Colors.purple,
|
||||
Colors.orange,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)
|
||||
: null,
|
||||
color: widget.unread
|
||||
? null
|
||||
: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(Avatar.defaultSize),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: widget.heroTag,
|
||||
child: Avatar(
|
||||
mxContent: widget.profile.avatarUrl,
|
||||
name: widget.profile.displayName,
|
||||
size: 72,
|
||||
fontSize: 26,
|
||||
),
|
||||
),
|
||||
if (widget.showEditFab)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: null,
|
||||
onPressed: () =>
|
||||
context.go('/rooms/stories/create'),
|
||||
child: const Icon(
|
||||
Icons.add_outlined,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (lastMessage != null) ...[
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 8,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Material(
|
||||
elevation: lastMessageBubbleElevation,
|
||||
shadowColor: lastMessageBubbleShadowColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
),
|
||||
color: lastMessageBubbleColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: Text(
|
||||
lastMessage,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 26.0,
|
||||
top: 4.0,
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: Material(
|
||||
elevation: lastMessageBubbleElevation,
|
||||
shadowColor:
|
||||
lastMessageBubbleShadowColor,
|
||||
borderRadius:
|
||||
BorderRadius.circular(99),
|
||||
color: lastMessageBubbleColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
widget.profile.displayName ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: widget.unread ? FontWeight.bold : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Room {
|
||||
String? get creatorId => getState(EventTypes.RoomCreate)?.senderId;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue