feat: Enhance roomlist context menu

This commit is contained in:
Christian Pauly 2020-10-02 15:50:59 +02:00
parent 41ceb84b47
commit 493b7000c6
3 changed files with 268 additions and 237 deletions

View file

@ -1,7 +1,6 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/chat.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
@ -18,11 +17,20 @@ import '../dialogs/send_file_dialog.dart';
class ChatListItem extends StatelessWidget { class ChatListItem extends StatelessWidget {
final Room room; final Room room;
final bool activeChat; final bool activeChat;
final bool selected;
final Function onForget; final Function onForget;
final Function onTap;
final Function onLongPress;
const ChatListItem(this.room, {this.activeChat = false, this.onForget}); const ChatListItem(this.room,
{this.activeChat = false,
this.selected = false,
this.onTap,
this.onLongPress,
this.onForget});
void clickAction(BuildContext context) async { void clickAction(BuildContext context) async {
if (onTap != null) return onTap();
if (!activeChat) { if (!activeChat) {
if (room.membership == Membership.invite && if (room.membership == Membership.invite &&
await SimpleDialogs(context) await SimpleDialogs(context)
@ -94,19 +102,7 @@ class ChatListItem extends StatelessWidget {
} }
} }
Future<void> _toggleFavouriteRoom(BuildContext context) => Future<void> archiveAction(BuildContext context) async {
SimpleDialogs(context).tryRequestWithLoadingDialog(
room.setFavourite(!room.isFavourite),
);
Future<void> _toggleMuted(BuildContext context) =>
SimpleDialogs(context).tryRequestWithLoadingDialog(
room.setPushRuleState(room.pushRuleState == PushRuleState.notify
? PushRuleState.mentions_only
: PushRuleState.notify),
);
Future<bool> archiveAction(BuildContext context) async {
{ {
if ([Membership.leave, Membership.ban].contains(room.membership)) { if ([Membership.leave, Membership.ban].contains(room.membership)) {
final success = await SimpleDialogs(context) final success = await SimpleDialogs(context)
@ -117,163 +113,115 @@ class ChatListItem extends StatelessWidget {
return success; return success;
} }
final confirmed = await SimpleDialogs(context).askConfirmation(); final confirmed = await SimpleDialogs(context).askConfirmation();
if (!confirmed) { if (!confirmed) return;
return false; await SimpleDialogs(context).tryRequestWithLoadingDialog(room.leave());
} return;
final success = await SimpleDialogs(context)
.tryRequestWithLoadingDialog(room.leave());
if (success == false) {
return false;
}
return true;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMuted = room.pushRuleState != PushRuleState.notify; final isMuted = room.pushRuleState != PushRuleState.notify;
final slideableKey = GlobalKey(); return Center(
return Slidable( child: Material(
key: slideableKey, color: chatListItemColor(context, activeChat, selected),
secondaryActions: <Widget>[ child: ListTile(
if ([Membership.join, Membership.invite].contains(room.membership)) onLongPress: onLongPress,
IconSlideAction( leading: Avatar(room.avatar, room.displayname),
caption: isMuted title: Row(
? L10n.of(context).unmuteChat children: <Widget>[
: L10n.of(context).muteChat, Expanded(
color: Colors.blueGrey, child: Text(
icon: room.getLocalizedDisplayname(L10n.of(context)),
isMuted ? Icons.notifications_active : Icons.notifications_off, maxLines: 1,
onTap: () => _toggleMuted(context), overflow: TextOverflow.ellipsis,
), softWrap: false,
if ([Membership.join, Membership.invite].contains(room.membership)) ),
IconSlideAction( ),
caption: room.isFavourite room.isFavourite
? L10n.of(context).unpin ? Padding(
: L10n.of(context).pin, padding: const EdgeInsets.only(left: 4.0),
color: Colors.blue, child: Icon(
icon: room.isFavourite ? Icons.favorite_border : Icons.favorite, Icons.favorite,
onTap: () => _toggleFavouriteRoom(context), color: Colors.grey[400],
), size: 16,
if ([Membership.join, Membership.invite].contains(room.membership)) ),
IconSlideAction( )
caption: L10n.of(context).leave, : Container(),
color: Colors.red, isMuted
icon: Icons.archive, ? Padding(
onTap: () => archiveAction(context), padding: const EdgeInsets.only(left: 4.0),
), child: Icon(
if ([Membership.leave, Membership.ban].contains(room.membership)) Icons.notifications_off,
IconSlideAction( color: Colors.grey[400],
caption: L10n.of(context).delete, size: 16,
color: Colors.red, ),
icon: Icons.delete_forever, )
onTap: () => archiveAction(context), : Container(),
), Padding(
], padding: const EdgeInsets.only(left: 4.0),
actionPane: SlidableDrawerActionPane(), child: Text(
child: Center( room.timeCreated.localizedTimeShort(context),
child: Material( style: TextStyle(
color: chatListItemColor(context, activeChat), color: Color(0xFF555555),
child: ListTile( fontSize: 13,
onLongPress: () => (slideableKey.currentState as SlidableState)
.open(actionType: SlideActionType.secondary),
leading: Avatar(room.avatar, room.displayname),
title: Row(
children: <Widget>[
Expanded(
child: Text(
room.getLocalizedDisplayname(L10n.of(context)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
), ),
), ),
room.isFavourite ),
? Padding( ],
padding: const EdgeInsets.only(left: 4.0),
child: Icon(
Icons.favorite,
color: Colors.grey[400],
size: 16,
),
)
: Container(),
isMuted
? Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Icon(
Icons.notifications_off,
color: Colors.grey[400],
size: 16,
),
)
: Container(),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
room.timeCreated.localizedTimeShort(context),
style: TextStyle(
color: Color(0xFF555555),
fontSize: 13,
),
),
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: room.membership == Membership.invite
? Text(
L10n.of(context).youAreInvitedToThisChat,
style: TextStyle(
color: Theme.of(context).primaryColor,
),
softWrap: false,
)
: Text(
room.lastEvent?.getLocalizedBody(
L10n.of(context),
withSenderNamePrefix: !room.isDirectChat ||
room.lastEvent.senderId ==
room.client.userID,
hideReply: true,
) ??
'',
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
),
),
SizedBox(width: 8),
room.notificationCount > 0
? Container(
padding: EdgeInsets.symmetric(horizontal: 5),
height: 20,
decoration: BoxDecoration(
color: room.highlightCount > 0
? Colors.red
: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
room.notificationCount.toString(),
style: TextStyle(color: Colors.white),
),
),
)
: Text(' '),
],
),
onTap: () => clickAction(context),
), ),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: room.membership == Membership.invite
? Text(
L10n.of(context).youAreInvitedToThisChat,
style: TextStyle(
color: Theme.of(context).primaryColor,
),
softWrap: false,
)
: Text(
room.lastEvent?.getLocalizedBody(
L10n.of(context),
withSenderNamePrefix: !room.isDirectChat ||
room.lastEvent.senderId == room.client.userID,
hideReply: true,
) ??
'',
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
),
),
SizedBox(width: 8),
room.notificationCount > 0
? Container(
padding: EdgeInsets.symmetric(horizontal: 5),
height: 20,
decoration: BoxDecoration(
color: room.highlightCount > 0
? Colors.red
: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
room.notificationCount.toString(),
style: TextStyle(color: Colors.white),
),
),
)
: Text(' '),
],
),
onTap: () => clickAction(context),
), ),
), ),
); );

View file

@ -112,18 +112,20 @@ final ThemeData amoledTheme = ThemeData.dark().copyWith(
), ),
); );
Color chatListItemColor(BuildContext context, bool activeChat) => Color chatListItemColor(BuildContext context, bool activeChat, bool selected) =>
Theme.of(context).brightness == Brightness.light selected
? activeChat ? Theme.of(context).primaryColor.withAlpha(50)
? Color(0xFFE8E8E8) : Theme.of(context).brightness == Brightness.light
: Colors.white ? activeChat
: activeChat ? Color(0xFFE8E8E8)
? ThemeSwitcherWidget.of(context).amoledEnabled : Colors.white
? Color(0xff121212) : activeChat
: Colors.black ? ThemeSwitcherWidget.of(context).amoledEnabled
: ThemeSwitcherWidget.of(context).amoledEnabled ? Color(0xff121212)
? Colors.black : Colors.black
: Color(0xff121212); : ThemeSwitcherWidget.of(context).amoledEnabled
? Colors.black
: Color(0xff121212);
Color blackWhiteColor(BuildContext context) => Color blackWhiteColor(BuildContext context) =>
Theme.of(context).brightness == Brightness.light Theme.of(context).brightness == Brightness.light

View file

@ -25,7 +25,7 @@ import 'new_group.dart';
import 'new_private_chat.dart'; import 'new_private_chat.dart';
import 'settings.dart'; import 'settings.dart';
enum SelectMode { normal, share } enum SelectMode { normal, share, select }
class ChatListView extends StatelessWidget { class ChatListView extends StatelessWidget {
@override @override
@ -59,9 +59,15 @@ class _ChatListState extends State<ChatList> {
PublicRoomsResponse publicRoomsResponse; PublicRoomsResponse publicRoomsResponse;
bool loadingPublicRooms = false; bool loadingPublicRooms = false;
String searchServer; String searchServer;
final _selectedRoomIds = <String>{};
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
void _toggleSelection(String roomId) =>
setState(() => _selectedRoomIds.contains(roomId)
? _selectedRoomIds.remove(roomId)
: _selectedRoomIds.add(roomId));
Future<void> waitForFirstSync(BuildContext context) async { Future<void> waitForFirstSync(BuildContext context) async {
var client = Matrix.of(context).client; var client = Matrix.of(context).client;
if (client.prevBatch?.isEmpty ?? true) { if (client.prevBatch?.isEmpty ?? true) {
@ -215,6 +221,39 @@ class _ChatListState extends State<ChatList> {
super.dispose(); super.dispose();
} }
Future<void> _toggleFavouriteRoom(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return SimpleDialogs(context).tryRequestWithLoadingDialog(
room.setFavourite(!room.isFavourite),
);
}
Future<void> _toggleMuted(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return SimpleDialogs(context).tryRequestWithLoadingDialog(
room.setPushRuleState(room.pushRuleState == PushRuleState.notify
? PushRuleState.mentions_only
: PushRuleState.notify),
);
}
Future<void> _archiveAction(BuildContext context) async {
final confirmed = await SimpleDialogs(context).askConfirmation();
if (!confirmed) return;
await SimpleDialogs(context)
.tryRequestWithLoadingDialog(_archiveSelectedRooms(context));
setState(() => null);
}
Future<void> _archiveSelectedRooms(BuildContext context) async {
final client = Matrix.of(context).client;
while (_selectedRoomIds.isNotEmpty) {
final roomId = _selectedRoomIds.first;
await client.getRoomById(roomId).leave();
_selectedRoomIds.remove(roomId);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<LoginState>( return StreamBuilder<LoginState>(
@ -232,10 +271,15 @@ class _ChatListState extends State<ChatList> {
stream: Matrix.of(context).onShareContentChanged.stream, stream: Matrix.of(context).onShareContentChanged.stream,
builder: (context, snapshot) { builder: (context, snapshot) {
final selectMode = Matrix.of(context).shareContent == null final selectMode = Matrix.of(context).shareContent == null
? SelectMode.normal ? _selectedRoomIds.isEmpty
? SelectMode.normal
: SelectMode.select
: SelectMode.share; : SelectMode.share;
if (selectMode == SelectMode.share) {
_selectedRoomIds.clear();
}
return Scaffold( return Scaffold(
drawer: selectMode == SelectMode.share drawer: selectMode != SelectMode.normal
? null ? null
: Drawer( : Drawer(
child: SafeArea( child: SafeArea(
@ -290,54 +334,81 @@ class _ChatListState extends State<ChatList> {
), ),
), ),
appBar: AppBar( appBar: AppBar(
centerTitle: false,
elevation: _scrolledToTop ? 0 : null, elevation: _scrolledToTop ? 0 : null,
leading: selectMode != SelectMode.share leading: selectMode == SelectMode.share
? null ? IconButton(
: IconButton(
icon: Icon(Icons.close), icon: Icon(Icons.close),
onPressed: () => onPressed: () =>
Matrix.of(context).shareContent = null, Matrix.of(context).shareContent = null,
), )
: selectMode == SelectMode.select
? IconButton(
icon: Icon(Icons.close),
onPressed: () =>
setState(_selectedRoomIds.clear),
)
: null,
titleSpacing: 0, titleSpacing: 0,
actions: selectMode != SelectMode.select
? null
: [
if (_selectedRoomIds.length == 1)
IconButton(
icon: Icon(Icons.favorite_border_outlined),
onPressed: () => _toggleFavouriteRoom(context),
),
if (_selectedRoomIds.length == 1)
IconButton(
icon: Icon(Icons.notifications_none),
onPressed: () => _toggleMuted(context),
),
IconButton(
icon: Icon(Icons.archive),
onPressed: () => _archiveAction(context),
),
],
title: selectMode == SelectMode.share title: selectMode == SelectMode.share
? Text(L10n.of(context).share) ? Text(L10n.of(context).share)
: Container( : selectMode == SelectMode.select
height: 40, ? Text(_selectedRoomIds.length.toString())
padding: EdgeInsets.only(right: 8), : Container(
child: Material( height: 40,
color: Theme.of(context).secondaryHeaderColor, padding: EdgeInsets.only(right: 8),
borderRadius: BorderRadius.circular(32), child: Material(
child: TextField( color: Theme.of(context).secondaryHeaderColor,
autocorrect: false, borderRadius: BorderRadius.circular(32),
controller: searchController, child: TextField(
focusNode: _searchFocusNode, autocorrect: false,
decoration: InputDecoration( controller: searchController,
contentPadding: EdgeInsets.only( focusNode: _searchFocusNode,
top: 8, decoration: InputDecoration(
bottom: 8, contentPadding: EdgeInsets.only(
left: 16, top: 8,
bottom: 8,
left: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32),
),
hintText: L10n.of(context).searchForAChat,
suffixIcon: searchMode
? IconButton(
icon: Icon(Icons.backspace),
onPressed: () => setState(() {
searchController.clear();
_searchFocusNode.unfocus();
}),
)
: null,
),
), ),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32),
),
hintText: L10n.of(context).searchForAChat,
suffixIcon: searchMode
? IconButton(
icon: Icon(Icons.backspace),
onPressed: () => setState(() {
searchController.clear();
_searchFocusNode.unfocus();
}),
)
: null,
), ),
), ),
),
),
), ),
floatingActionButton: floatingActionButton:
(AdaptivePageLayout.columnMode(context) || (AdaptivePageLayout.columnMode(context) ||
selectMode == SelectMode.share) selectMode != SelectMode.normal)
? null ? null
: FloatingActionButton( : FloatingActionButton(
child: Icon(Icons.add), child: Icon(Icons.add),
@ -436,34 +507,32 @@ class _ChatListState extends State<ChatList> {
itemBuilder: itemBuilder:
(BuildContext context, int i) { (BuildContext context, int i) {
if (i == 0) { if (i == 0) {
final displayPresences = directChats
.isNotEmpty &&
selectMode == SelectMode.normal;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
(directChats.isEmpty || AnimatedContainer(
selectMode == duration: Duration(
SelectMode.share) milliseconds: 500),
? Container() height:
: PreferredSize( displayPresences ? 78 : 0,
preferredSize: child: !displayPresences
Size.fromHeight(82), ? null
child: Container( : ListView.builder(
height: 78, scrollDirection:
child: Axis.horizontal,
ListView.builder( itemCount: directChats
scrollDirection: .length,
Axis.horizontal, itemBuilder: (BuildContext
itemCount: context,
directChats int i) =>
.length, PresenceListItem(
itemBuilder: (BuildContext directChats[
context, i]),
int i) =>
PresenceListItem(
directChats[
i]),
),
), ),
), ),
], ],
); );
} }
@ -471,6 +540,18 @@ class _ChatListState extends State<ChatList> {
return i < rooms.length return i < rooms.length
? ChatListItem( ? ChatListItem(
rooms[i], rooms[i],
selected: _selectedRoomIds
.contains(rooms[i].id),
onTap: selectMode ==
SelectMode.select
? () => _toggleSelection(
rooms[i].id)
: null,
onLongPress: selectMode !=
SelectMode.share
? () => _toggleSelection(
rooms[i].id)
: null,
activeChat: activeChat:
widget.activeChat == widget.activeChat ==
rooms[i].id, rooms[i].id,