Merge branch 'krille/revert-new-design' into 'main'

change: Revert new design

See merge request famedly/fluffychat!381
This commit is contained in:
Krille Fear 2021-02-13 12:01:16 +00:00
commit d7d3b301fd
12 changed files with 1179 additions and 1335 deletions

View File

@ -0,0 +1,158 @@
import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/matrix.dart';
import 'package:flutter/material.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import '../utils/client_presence_extension.dart';
import '../utils/presence_extension.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'avatar.dart';
class HorizontalStoriesList extends StatefulWidget {
final String searchQuery;
const HorizontalStoriesList({Key key, this.searchQuery = ''})
: super(key: key);
@override
_HorizontalStoriesListState createState() => _HorizontalStoriesListState();
}
class _HorizontalStoriesListState extends State<HorizontalStoriesList> {
StreamSubscription _onSync;
@override
void dispose() {
_onSync?.cancel();
super.dispose();
}
DateTime _lastSetState = DateTime.now();
Timer _coolDown;
void _updateView() {
_lastSetState = DateTime.now();
setState(() => null);
}
static const double height = 68.0;
@override
Widget build(BuildContext context) {
_onSync ??= Matrix.of(context).client.onSync.stream.listen((_) {
if (DateTime.now().millisecondsSinceEpoch -
_lastSetState.millisecondsSinceEpoch <
1000) {
_coolDown?.cancel();
_coolDown = Timer(Duration(seconds: 1), _updateView);
} else {
_updateView();
}
});
final contactList = Matrix.of(context)
.client
.contactList
.where((p) =>
p.senderId.toLowerCase().contains(widget.searchQuery.toLowerCase()))
.toList();
return AnimatedContainer(
height: height,
duration: Duration(milliseconds: 300),
child: contactList.isEmpty
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: contactList.length,
itemBuilder: (context, i) =>
_StoriesListTile(story: contactList[i]),
),
);
}
}
class _StoriesListTile extends StatelessWidget {
final Presence story;
const _StoriesListTile({
Key key,
@required this.story,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final hasStatusMessage = story.presence.statusMsg?.isNotEmpty ?? false;
return FutureBuilder<Profile>(
future: Matrix.of(context).client.getProfileFromUserId(story.senderId),
builder: (context, snapshot) {
final displayname =
snapshot.data?.displayname ?? story.senderId.localpart;
final avatarUrl = snapshot.data?.avatarUrl;
return Container(
width: Avatar.defaultSize + 32,
height: _HorizontalStoriesListState.height,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () async {
if (story.senderId == Matrix.of(context).client.userID) {
await showOkAlertDialog(
context: context,
title: displayname,
message: story.presence.statusMsg,
okLabel: L10n.of(context).close,
);
return;
}
if (hasStatusMessage) {
if (OkCancelResult.ok !=
await showOkCancelAlertDialog(
context: context,
title: displayname,
message: story.presence.statusMsg,
okLabel: L10n.of(context).sendAMessage,
cancelLabel: L10n.of(context).close,
)) {
return;
}
}
final roomId = await Matrix.of(context)
.client
.startDirectChat(story.senderId);
await AdaptivePageLayout.of(context)
.pushNamedAndRemoveUntilIsFirst('/rooms/${roomId}');
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: Avatar.defaultSize,
height: Avatar.defaultSize,
child: Stack(
children: [
Center(child: Avatar(avatarUrl, displayname)),
Align(
alignment: Alignment.bottomRight,
child: Icon(
Icons.circle,
color: story.color,
size: 12,
),
),
],
),
),
SizedBox(height: 4),
Text(displayname.split(' ').first,
style: TextStyle(
fontWeight: hasStatusMessage ? FontWeight.bold : null,
color: hasStatusMessage
? Theme.of(context).accentColor
: null,
)),
],
),
),
);
});
}
}

View File

@ -1,71 +0,0 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/avatar.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../utils/presence_extension.dart';
import '../matrix.dart';
class ContactListTile extends StatelessWidget {
final Presence contact;
const ContactListTile({Key key, @required this.contact}) : super(key: key);
@override
Widget build(BuildContext context) {
var statusMsg = contact.presence?.statusMsg?.isNotEmpty ?? false
? contact.presence.statusMsg
: null;
return FutureBuilder<Profile>(
future:
Matrix.of(context).client.getProfileFromUserId(contact.senderId),
builder: (context, snapshot) {
final displayname =
snapshot.data?.displayname ?? contact.senderId.localpart;
final avatarUrl = snapshot.data?.avatarUrl;
return ListTile(
leading: Avatar(avatarUrl, displayname),
title: Row(
children: [
Icon(Icons.circle, color: contact.color, size: 10),
SizedBox(width: 4),
Expanded(
child: Text(
displayname,
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: statusMsg == null
? Text(contact.getLocalizedLastActiveAgo(context))
: Row(
children: [
Icon(Icons.edit_outlined,
color: Theme.of(context).accentColor, size: 12),
SizedBox(width: 2),
Expanded(
child: Text(
statusMsg,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color:
Theme.of(context).textTheme.bodyText1.color,
),
),
),
],
),
onTap: () async {
if (contact.senderId == Matrix.of(context).client.userID) {
return;
}
final roomId = await User(contact.senderId,
room: Room(id: '', client: Matrix.of(context).client))
.startDirectChat();
await AdaptivePageLayout.of(context)
.pushNamedAndRemoveUntilIsFirst('/rooms/${roomId}');
});
});
}
}

View File

@ -5,7 +5,8 @@ import 'package:fluffychat/views/archive.dart';
import 'package:fluffychat/views/chat.dart';
import 'package:fluffychat/views/chat_details.dart';
import 'package:fluffychat/views/chat_encryption_settings.dart';
import 'package:fluffychat/views/home_view.dart';
import 'package:fluffychat/views/discover.dart';
import 'package:fluffychat/views/chat_list.dart';
import 'package:fluffychat/views/chat_permissions_settings.dart';
import 'package:fluffychat/views/empty_page.dart';
import 'package:fluffychat/views/homeserver_picker.dart';
@ -15,6 +16,7 @@ import 'package:fluffychat/views/log_view.dart';
import 'package:fluffychat/views/login.dart';
import 'package:fluffychat/views/new_group.dart';
import 'package:fluffychat/views/new_private_chat.dart';
import 'package:fluffychat/views/settings.dart';
import 'package:fluffychat/views/settings_3pid.dart';
import 'package:fluffychat/views/settings_devices.dart';
import 'package:fluffychat/views/settings_emotes.dart';
@ -62,14 +64,14 @@ class FluffyRoutes {
switch (parts[1]) {
case '':
return ViewData(
mainView: (_) => HomeView(),
mainView: (_) => ChatList(),
emptyView: (_) => EmptyPage(),
);
case 'rooms':
final roomId = parts[2];
if (parts.length == 3) {
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId),
);
} else if (parts.length == 4) {
@ -77,44 +79,44 @@ class FluffyRoutes {
switch (action) {
case 'details':
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId),
rightView: (_) => ChatDetails(roomId),
);
case 'encryption':
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId),
rightView: (_) => ChatEncryptionSettings(roomId),
);
case 'permissions':
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId),
rightView: (_) => ChatPermissionsSettings(roomId),
);
case 'invite':
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId),
rightView: (_) => InvitationSelection(roomId),
);
case 'emotes':
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId),
rightView: (_) => MultipleEmotesSettings(roomId),
);
default:
return ViewData(
leftView: (_) => HomeView(activeChat: roomId),
leftView: (_) => ChatList(activeChat: roomId),
mainView: (_) => Chat(roomId,
scrollToEventId: action.sigil == '\$' ? action : null),
);
}
}
return ViewData(
mainView: (_) => HomeView(),
mainView: (_) => ChatList(),
emptyView: (_) => EmptyPage(),
);
case 'archive':
@ -128,31 +130,42 @@ class FluffyRoutes {
);
case 'newgroup':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => ChatList(),
mainView: (_) => NewGroup(),
);
case 'newprivatechat':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => ChatList(),
mainView: (_) => NewPrivateChat(),
);
case 'discover':
if (parts.length == 3) {
return ViewData(
mainView: (_) => Discover(alias: parts[2]),
emptyView: (_) => EmptyPage(),
);
}
return ViewData(
mainView: (_) => Discover(),
emptyView: (_) => EmptyPage(),
);
case 'settings':
if (parts.length == 3) {
final action = parts[2];
switch (action) {
case '3pid':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => Settings(),
mainView: (_) => Settings3Pid(),
);
case 'devices':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => Settings(),
mainView: (_) => DevicesSettings(),
);
case 'emotes':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => Settings(),
mainView: (_) => EmotesSettings(
room: ((settings.arguments ?? {}) as Map)['room'],
stateKey: ((settings.arguments ?? {}) as Map)['stateKey'],
@ -160,25 +173,30 @@ class FluffyRoutes {
);
case 'ignore':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => Settings(),
mainView: (_) => SettingsIgnoreList(
initialUserId: settings.arguments,
),
);
case 'notifications':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => Settings(),
mainView: (_) => SettingsNotifications(),
);
case 'style':
return ViewData(
leftView: (_) => HomeView(),
leftView: (_) => Settings(),
mainView: (_) => SettingsStyle(),
);
}
} else {
return ViewData(
mainView: (_) => Settings(),
emptyView: (_) => EmptyPage(),
);
}
return ViewData(
mainView: (_) => HomeView(),
mainView: (_) => ChatList(),
emptyView: (_) => EmptyPage(),
);
}

View File

@ -67,7 +67,6 @@ abstract class FluffyThemes {
fillColor: lighten(AppConfig.primaryColor, .51),
),
appBarTheme: AppBarTheme(
elevation: 1,
brightness: Brightness.light,
color: Colors.white,
textTheme: TextTheme(
@ -117,7 +116,6 @@ abstract class FluffyThemes {
),
),
appBarTheme: AppBarTheme(
elevation: 1,
brightness: Brightness.dark,
color: Color(0xff1D1D1D),
textTheme: TextTheme(

486
lib/views/chat_list.dart Normal file
View File

@ -0,0 +1,486 @@
import 'dart:async';
import 'dart:io';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/connection_status_header.dart';
import 'package:fluffychat/components/default_app_bar_search_field.dart';
import 'package:fluffychat/components/horizontal_stories_list.dart';
import 'package:fluffychat/components/list_items/chat_list_item.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:flutter/cupertino.dart';
import 'package:fluffychat/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import '../components/matrix.dart';
import '../utils/matrix_file_extension.dart';
import '../utils/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum SelectMode { normal, share, select }
class ChatList extends StatefulWidget {
final String activeChat;
const ChatList({this.activeChat, Key key}) : super(key: key);
@override
_ChatListState createState() => _ChatListState();
}
class _ChatListState extends State<ChatList> {
StreamSubscription _intentDataStreamSubscription;
StreamSubscription _intentFileStreamSubscription;
AppBar appBar;
bool get searchMode => searchController.text?.isNotEmpty ?? false;
final TextEditingController searchController = TextEditingController();
final _selectedRoomIds = <String>{};
final ScrollController _scrollController = ScrollController();
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
if (files?.isEmpty ?? true) return;
AdaptivePageLayout.of(context).popUntilIsFirst();
final file = File(files.first.path);
Matrix.of(context).shareContent = {
'msgtype': 'chat.fluffy.shared_file',
'file': MatrixFile(
bytes: file.readAsBytesSync(),
name: file.path,
).detectFileType,
};
}
void _processIncomingSharedText(String text) {
if (text == null) return;
AdaptivePageLayout.of(context).popUntilIsFirst();
if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
!RegExp(r'\s').hasMatch(text))) {
UrlLauncher(context, text).openMatrixToUrl();
return;
}
Matrix.of(context).shareContent = {
'msgtype': 'm.text',
'body': text,
};
}
void _initReceiveSharingIntent() {
if (!PlatformInfos.isMobile) return;
// For sharing images coming from outside the app while the app is in the memory
_intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream()
.listen(_processIncomingSharedFiles, onError: print);
// For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles);
// For sharing or opening urls/text coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream()
.listen(_processIncomingSharedText, onError: print);
// For sharing or opening urls/text coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText);
}
@override
void initState() {
_initReceiveSharingIntent();
super.initState();
}
@override
void dispose() {
_intentDataStreamSubscription?.cancel();
_intentFileStreamSubscription?.cancel();
super.dispose();
}
void _onPopupMenuButtonSelect(ChatListPopupMenuItemActions action) {
switch (action) {
case ChatListPopupMenuItemActions.createGroup:
AdaptivePageLayout.of(context).pushNamed('/newgroup');
break;
case ChatListPopupMenuItemActions.discover:
AdaptivePageLayout.of(context).pushNamed('/discover');
break;
case ChatListPopupMenuItemActions.setStatus:
_setStatus();
break;
case ChatListPopupMenuItemActions.inviteContact:
FluffyShare.share(
L10n.of(context).inviteText(Matrix.of(context).client.userID,
'https://matrix.to/#/${Matrix.of(context).client.userID}'),
context);
break;
case ChatListPopupMenuItemActions.settings:
AdaptivePageLayout.of(context).pushNamed('/settings');
break;
}
}
void _setStatus() async {
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).setStatus,
textFields: [
DialogTextField(
hintText: L10n.of(context).statusExampleMessage,
),
]);
if (input == null) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.sendPresence(
Matrix.of(context).client.userID,
PresenceType.online,
statusMsg: input.single,
),
);
}
void _toggleSelection(String roomId) {
setState(() => _selectedRoomIds.contains(roomId)
? _selectedRoomIds.remove(roomId)
: _selectedRoomIds.add(roomId));
}
Future<void> _toggleUnread(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return showFutureLoadingDialog(
context: context,
future: () => room.setUnread(!room.isUnread),
);
}
Future<void> _toggleFavouriteRoom(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return showFutureLoadingDialog(
context: context,
future: () => room.setFavourite(!room.isFavourite),
);
}
Future<void> _toggleMuted(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return showFutureLoadingDialog(
context: context,
future: () => room.setPushRuleState(
room.pushRuleState == PushRuleState.notify
? PushRuleState.mentions_only
: PushRuleState.notify),
);
}
Future<void> _archiveAction(BuildContext context) async {
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
) ==
OkCancelResult.ok;
if (!confirmed) return;
await showFutureLoadingDialog(
context: context,
future: () => _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();
_toggleSelection(roomId);
}
}
Future<void> waitForFirstSync(BuildContext context) async {
var client = Matrix.of(context).client;
if (client.prevBatch?.isEmpty ?? true) {
await client.onFirstSync.stream.first;
}
return true;
}
final GlobalKey<DefaultAppBarSearchFieldState> _searchFieldKey = GlobalKey();
@override
Widget build(BuildContext context) {
return StreamBuilder<Object>(
stream: Matrix.of(context).onShareContentChanged.stream,
builder: (_, __) {
final selectMode = Matrix.of(context).shareContent != null
? SelectMode.share
: _selectedRoomIds.isEmpty
? SelectMode.normal
: SelectMode.select;
return Scaffold(
appBar: appBar ??
AppBar(
leading: selectMode == SelectMode.normal
? null
: IconButton(
icon: Icon(Icons.close_outlined),
onPressed: () => selectMode == SelectMode.share
? setState(
() => Matrix.of(context).shareContent = null)
: setState(() => _selectedRoomIds.clear()),
),
centerTitle: false,
actions: selectMode == SelectMode.share
? null
: selectMode == SelectMode.select
? [
if (_selectedRoomIds.length == 1)
IconButton(
tooltip: L10n.of(context).toggleUnread,
icon: Icon(Matrix.of(context)
.client
.getRoomById(_selectedRoomIds.single)
.isUnread
? Icons.mark_chat_read_outlined
: Icons.mark_chat_unread_outlined),
onPressed: () => _toggleUnread(context),
),
if (_selectedRoomIds.length == 1)
IconButton(
tooltip: L10n.of(context).toggleFavorite,
icon: Icon(Icons.push_pin_outlined),
onPressed: () =>
_toggleFavouriteRoom(context),
),
if (_selectedRoomIds.length == 1)
IconButton(
icon: Icon(Matrix.of(context)
.client
.getRoomById(
_selectedRoomIds.single)
.pushRuleState ==
PushRuleState.notify
? Icons.notifications_off_outlined
: Icons.notifications_outlined),
tooltip: L10n.of(context).toggleMuted,
onPressed: () => _toggleMuted(context),
),
IconButton(
icon: Icon(Icons.archive_outlined),
tooltip: L10n.of(context).archive,
onPressed: () => _archiveAction(context),
),
]
: [
IconButton(
icon: Icon(Icons.search_outlined),
onPressed: () async {
await _scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
WidgetsBinding.instance.addPostFrameCallback(
(_) => _searchFieldKey.currentState
.requestFocus(),
);
},
),
PopupMenuButton<ChatListPopupMenuItemActions>(
onSelected: _onPopupMenuButtonSelect,
itemBuilder: (_) => [
PopupMenuItem(
value: ChatListPopupMenuItemActions
.createGroup,
child: Row(
children: [
Icon(Icons.group_add_outlined),
SizedBox(width: 12),
Text(L10n.of(context).createNewGroup),
],
),
),
PopupMenuItem(
value:
ChatListPopupMenuItemActions.discover,
child: Row(
children: [
Icon(Icons.group_work_outlined),
SizedBox(width: 12),
Text(L10n.of(context).discoverGroups),
],
),
),
PopupMenuItem(
value:
ChatListPopupMenuItemActions.setStatus,
child: Row(
children: [
Icon(Icons.edit_outlined),
SizedBox(width: 12),
Text(L10n.of(context).setStatus),
],
),
),
PopupMenuItem(
value: ChatListPopupMenuItemActions
.inviteContact,
child: Row(
children: [
Icon(Icons.share_outlined),
SizedBox(width: 12),
Text(L10n.of(context).inviteContact),
],
),
),
PopupMenuItem(
value:
ChatListPopupMenuItemActions.settings,
child: Row(
children: [
Icon(Icons.settings_outlined),
SizedBox(width: 12),
Text(L10n.of(context).settings),
],
),
),
],
),
],
title: Text(selectMode == SelectMode.share
? L10n.of(context).share
: selectMode == SelectMode.select
? L10n.of(context).numberSelected(
_selectedRoomIds.length.toString())
: AppConfig.applicationName),
),
body: Column(children: [
ConnectionStatusHeader(),
Expanded(
child: StreamBuilder(
stream: Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.hasRoomUpdate),
builder: (context, snapshot) {
return FutureBuilder<void>(
future: waitForFirstSync(context),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
var rooms = List<Room>.from(
Matrix.of(context).client.rooms);
rooms.removeWhere((room) =>
room.lastEvent == null ||
(searchMode &&
!room.displayname.toLowerCase().contains(
searchController.text.toLowerCase() ??
'')));
if (rooms.isEmpty && (!searchMode)) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
searchMode
? Icons.search_outlined
: Icons.maps_ugc_outlined,
size: 80,
color: Colors.grey,
),
Center(
child: Text(
searchMode
? L10n.of(context).noRoomsFound
: L10n.of(context).startYourFirstChat,
textAlign: TextAlign.start,
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
}
final totalCount = rooms.length;
return ListView.builder(
controller: _scrollController,
itemCount: totalCount + 1,
itemBuilder: (BuildContext context, int i) => i ==
0
? Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.all(12),
child: DefaultAppBarSearchField(
key: _searchFieldKey,
hintText: L10n.of(context).search,
prefixIcon:
Icon(Icons.search_outlined),
searchController: searchController,
onChanged: (_) =>
setState(() => null),
padding: EdgeInsets.zero,
),
),
if (selectMode == SelectMode.normal)
Padding(
padding:
const EdgeInsets.only(top: 4.0),
child: HorizontalStoriesList(
searchQuery:
searchController.text,
),
),
],
)
: ChatListItem(
rooms[i - 1],
selected: _selectedRoomIds
.contains(rooms[i - 1].id),
onTap: selectMode == SelectMode.select
? () =>
_toggleSelection(rooms[i - 1].id)
: null,
onLongPress: () =>
_toggleSelection(rooms[i - 1].id),
activeChat:
widget.activeChat == rooms[i - 1].id,
),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}),
),
]),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add_outlined),
onPressed: () => AdaptivePageLayout.of(context)
.pushNamedAndRemoveUntilIsFirst('/newprivatechat'),
),
);
});
}
}
enum ChatListPopupMenuItemActions {
createGroup,
discover,
setStatus,
inviteContact,
settings,
}

289
lib/views/discover.dart Normal file
View File

@ -0,0 +1,289 @@
import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/avatar.dart';
import 'package:fluffychat/components/default_app_bar_search_field.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/components/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../utils/localized_exception_extension.dart';
class Discover extends StatefulWidget {
final String alias;
const Discover({
Key key,
this.alias,
}) : super(key: key);
@override
_DiscoverState createState() => _DiscoverState();
}
class _DiscoverState extends State<Discover> {
Future<PublicRoomsResponse> _publicRoomsResponse;
String _lastServer;
Timer _coolDown;
String _genericSearchTerm;
void _search(BuildContext context, String query) async {
_coolDown?.cancel();
_coolDown = Timer(
Duration(milliseconds: 500),
() => setState(() {
_genericSearchTerm = query;
_publicRoomsResponse = null;
}),
);
}
Future<String> _joinRoomAndWait(
BuildContext context,
String roomId,
String alias,
) async {
if (Matrix.of(context).client.getRoomById(roomId) != null) {
return roomId;
}
final newRoomId = await Matrix.of(context)
.client
.joinRoomOrAlias(alias?.isNotEmpty ?? false ? alias : roomId);
await Matrix.of(context)
.client
.onRoomUpdate
.stream
.firstWhere((r) => r.id == newRoomId);
return newRoomId;
}
void _joinGroupAction(BuildContext context, PublicRoom room) async {
if (await showOkCancelAlertDialog(
context: context,
okLabel: L10n.of(context).joinRoom,
title: '${room.name} (${room.numJoinedMembers ?? 0})',
message: room.topic ?? L10n.of(context).noDescription,
) ==
OkCancelResult.cancel) {
return;
}
final success = await showFutureLoadingDialog(
context: context,
future: () => _joinRoomAndWait(
context,
room.roomId,
room.canonicalAlias ?? room.aliases.first,
),
);
if (success.error == null) {
await AdaptivePageLayout.of(context)
.pushNamedAndRemoveUntilIsFirst('/rooms/${success.result}');
}
}
String _server;
void _setServer(BuildContext context) async {
final newServer = await showTextInputDialog(
title: L10n.of(context).changeTheHomeserver,
context: context,
textFields: [
DialogTextField(
prefixText: 'https://',
hintText: Matrix.of(context).client.homeserver.host,
initialText: _server,
keyboardType: TextInputType.url,
)
]);
if (newServer == null) return;
setState(() {
_server = newServer.single;
});
}
@override
void initState() {
_genericSearchTerm = widget.alias;
super.initState();
}
@override
Widget build(BuildContext context) {
final server = _genericSearchTerm?.isValidMatrixId ?? false
? _genericSearchTerm.domain
: _server;
if (_lastServer != server) {
_lastServer = server;
_publicRoomsResponse = null;
}
_publicRoomsResponse ??= Matrix.of(context)
.client
.searchPublicRooms(
server: server,
genericSearchTerm: _genericSearchTerm,
)
.catchError((error) {
if (widget.alias == null) {
throw error;
}
return PublicRoomsResponse.fromJson({
'chunk': [],
});
}).then((PublicRoomsResponse res) {
if (widget.alias != null &&
!res.chunk.any((room) =>
(room.aliases?.contains(widget.alias) ?? false) ||
room.canonicalAlias == widget.alias)) {
// we have to tack on the original alias
res.chunk.add(PublicRoom.fromJson(<String, dynamic>{
'aliases': [widget.alias],
'name': widget.alias,
}));
}
return res;
});
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).discoverGroups),
actions: [
FlatButton(
child: Text(
server ?? Matrix.of(context).client.userID.domain,
style: TextStyle(color: Theme.of(context).primaryColor),
),
onPressed: () => _setServer(context),
),
],
),
body: ListView(
children: [
Padding(
padding: EdgeInsets.all(12),
child: DefaultAppBarSearchField(
hintText: L10n.of(context).search,
prefixIcon: Icon(Icons.search_outlined),
onChanged: (t) => _search(context, t),
padding: EdgeInsets.zero,
),
),
FutureBuilder<PublicRoomsResponse>(
future: _publicRoomsResponse,
builder: (BuildContext context,
AsyncSnapshot<PublicRoomsResponse> snapshot) {
if (snapshot.hasError) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 32),
Icon(
Icons.error_outlined,
size: 80,
color: Colors.grey,
),
Center(
child: Text(
snapshot.error.toLocalizedString(context),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(child: CircularProgressIndicator());
}
final publicRoomsResponse = snapshot.data;
if (publicRoomsResponse.chunk.isEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 32),
Icon(
Icons.search_outlined,
size: 80,
color: Colors.grey,
),
Center(
child: Text(
L10n.of(context).noPublicRoomsFound,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
}
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.all(12),
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: publicRoomsResponse.chunk.length,
itemBuilder: (BuildContext context, int i) => Material(
elevation: 2,
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: () => _joinGroupAction(
context,
publicRoomsResponse.chunk[i],
),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
Uri.parse(
publicRoomsResponse.chunk[i].avatarUrl ??
''),
publicRoomsResponse.chunk[i].name),
Text(
publicRoomsResponse.chunk[i].name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 1,
textAlign: TextAlign.center,
),
Text(
L10n.of(context).countParticipants(
publicRoomsResponse
.chunk[i].numJoinedMembers ??
0),
style: TextStyle(fontSize: 10.5),
maxLines: 1,
textAlign: TextAlign.center,
),
Text(
publicRoomsResponse.chunk[i].topic ??
L10n.of(context).noDescription,
maxLines: 4,
textAlign: TextAlign.center,
),
],
),
),
),
),
);
}),
],
),
);
}
}

View File

@ -1,291 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/home_view_parts/discover.dart';
import 'package:fluffychat/views/share_view.dart';
import 'package:flutter/cupertino.dart';
import 'package:fluffychat/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import '../components/matrix.dart';
import '../utils/matrix_file_extension.dart';
import '../utils/url_launcher.dart';
import 'home_view_parts/chat_list.dart';
import 'home_view_parts/settings.dart';
import 'home_view_parts/contact_list.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum SelectMode { normal, share, select }
class HomeView extends StatefulWidget {
final String activeChat;
const HomeView({this.activeChat, Key key}) : super(key: key);
@override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> with TickerProviderStateMixin {
@override
void initState() {
_initReceiveSharingIntent();
_pageController = TabController(length: 4, vsync: this, initialIndex: 1);
_pageController.addListener(_updateCurrentIndex);
super.initState();
}
void _updateCurrentIndex() =>
setState(() => currentIndex = _pageController.index);
int currentIndex = 1;
StreamSubscription _intentDataStreamSubscription;
StreamSubscription _intentFileStreamSubscription;
StreamSubscription _onShareContentChanged;
AppBar appBar;
TabController _pageController;
void _onShare(Map<String, dynamic> content) {
if (content != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ShareView(),
),
),
);
}
}
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
if (files?.isEmpty ?? true) return;
AdaptivePageLayout.of(context).popUntilIsFirst();
final file = File(files.first.path);
Matrix.of(context).shareContent = {
'msgtype': 'chat.fluffy.shared_file',
'file': MatrixFile(
bytes: file.readAsBytesSync(),
name: file.path,
).detectFileType,
};
}
void _processIncomingSharedText(String text) {
if (text == null) return;
AdaptivePageLayout.of(context).popUntilIsFirst();
if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
!RegExp(r'\s').hasMatch(text))) {
UrlLauncher(context, text).openMatrixToUrl();
return;
}
Matrix.of(context).shareContent = {
'msgtype': 'm.text',
'body': text,
};
}
void _initReceiveSharingIntent() {
if (!PlatformInfos.isMobile) return;
// For sharing images coming from outside the app while the app is in the memory
_intentFileStreamSubscription = ReceiveSharingIntent.getMediaStream()
.listen(_processIncomingSharedFiles, onError: print);
// For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then(_processIncomingSharedFiles);
// For sharing or opening urls/text coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream()
.listen(_processIncomingSharedText, onError: print);
// For sharing or opening urls/text coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText);
}
@override
void dispose() {
_intentDataStreamSubscription?.cancel();
_intentFileStreamSubscription?.cancel();
_pageController.removeListener(_updateCurrentIndex);
super.dispose();
}
String _server;
void _setServer(BuildContext context) async {
final newServer = await showTextInputDialog(
title: L10n.of(context).changeTheHomeserver,
context: context,
textFields: [
DialogTextField(
prefixText: 'https://',
hintText: Matrix.of(context).client.homeserver.host,
initialText: _server,
keyboardType: TextInputType.url,
)
]);
if (newServer == null) return;
setState(() {
_server = newServer.single;
});
}
void _setStatus() async {
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).setStatus,
textFields: [
DialogTextField(
hintText: L10n.of(context).statusExampleMessage,
),
]);
if (input == null) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.sendPresence(
Matrix.of(context).client.userID,
PresenceType.online,
statusMsg: input.single,
),
);
}
void _onFabTab() {
switch (currentIndex) {
case 0:
_setStatus();
break;
case 1:
AdaptivePageLayout.of(context)
.pushNamedAndRemoveUntilIsFirst('/newprivatechat');
break;
case 2:
_setServer(context);
break;
}
}
final StreamController<int> _onAppBarButtonTap =
StreamController<int>.broadcast();
@override
Widget build(BuildContext context) {
_onShareContentChanged ??=
Matrix.of(context).onShareContentChanged.stream.listen(_onShare);
IconData fabIcon;
String title;
switch (currentIndex) {
case 0:
fabIcon = Icons.edit_outlined;
title = L10n.of(context).contacts;
break;
case 1:
fabIcon = Icons.add_outlined;
title = AppConfig.applicationName;
break;
case 2:
fabIcon = Icons.domain_outlined;
title = L10n.of(context).discover;
break;
case 3:
title = L10n.of(context).settings;
break;
}
return Scaffold(
appBar: appBar ??
AppBar(
centerTitle: false,
actions: [
IconButton(
icon: Icon(currentIndex == 3
? Icons.exit_to_app_outlined
: Icons.search_outlined),
onPressed: () => _pageController.indexIsChanging
? null
: _onAppBarButtonTap.add(currentIndex),
),
],
title: Text(title),
),
body: Column(
children: [
Expanded(
child: TabBarView(
controller: _pageController,
children: [
ContactList(onAppBarButtonTap: _onAppBarButtonTap.stream),
ChatList(
onCustomAppBar: (appBar) =>
setState(() => this.appBar = appBar),
onAppBarButtonTap: _onAppBarButtonTap.stream,
),
Discover(
server: _server,
onAppBarButtonTap: _onAppBarButtonTap.stream),
Settings(onAppBarButtonTap: _onAppBarButtonTap.stream),
],
),
),
Divider(height: 1),
],
),
floatingActionButton: fabIcon == null
? null
: FloatingActionButton(
child: Icon(fabIcon),
onPressed: _onFabTab,
foregroundColor:
currentIndex == 2 ? Theme.of(context).accentColor : null,
backgroundColor: currentIndex == 2
? Theme.of(context).scaffoldBackgroundColor
: null,
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomNavigationBar(
elevation: 0,
unselectedItemColor: Theme.of(context).textTheme.bodyText1.color,
currentIndex: currentIndex,
showSelectedLabels: true,
showUnselectedLabels: false,
type: BottomNavigationBarType.fixed,
backgroundColor: Theme.of(context).appBarTheme.color,
onTap: (i) {
_pageController.animateTo(i);
setState(() => currentIndex = i);
},
items: [
BottomNavigationBarItem(
label: L10n.of(context).contacts,
icon: Icon(Icons.people_outlined),
),
BottomNavigationBarItem(
label: L10n.of(context).messages,
icon: Icon(CupertinoIcons.chat_bubble_2),
),
BottomNavigationBarItem(
label: L10n.of(context).discover,
icon: Icon(CupertinoIcons.compass),
),
BottomNavigationBarItem(
label: L10n.of(context).settings,
icon: Icon(Icons.settings_outlined),
),
],
),
);
}
}

View File

@ -1,273 +0,0 @@
import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/connection_status_header.dart';
import 'package:fluffychat/components/default_app_bar_search_field.dart';
import 'package:fluffychat/components/list_items/chat_list_item.dart';
import 'package:fluffychat/components/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
enum SelectMode { normal, select }
class ChatList extends StatefulWidget {
final String activeChat;
final void Function(AppBar appBar) onCustomAppBar;
final Stream onAppBarButtonTap;
const ChatList({
Key key,
this.activeChat,
this.onCustomAppBar,
this.onAppBarButtonTap,
}) : super(key: key);
@override
_ChatListState createState() => _ChatListState();
}
class _ChatListState extends State<ChatList> {
bool get searchMode => searchController.text?.isNotEmpty ?? false;
final TextEditingController searchController = TextEditingController();
final _selectedRoomIds = <String>{};
final ScrollController _scrollController = ScrollController();
StreamSubscription _onAppBarButtonTapSub;
final GlobalKey<DefaultAppBarSearchFieldState> _searchField = GlobalKey();
@override
void initState() {
_onAppBarButtonTapSub =
widget.onAppBarButtonTap.where((i) => i == 1).listen((_) async {
await _scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
WidgetsBinding.instance.addPostFrameCallback(
(_) => _searchField.currentState.requestFocus(),
);
});
super.initState();
}
@override
void dispose() {
_onAppBarButtonTapSub?.cancel();
super.dispose();
}
void _toggleSelection(String roomId) {
setState(() => _selectedRoomIds.contains(roomId)
? _selectedRoomIds.remove(roomId)
: _selectedRoomIds.add(roomId));
widget.onCustomAppBar(
_selectedRoomIds.isEmpty
? null
: AppBar(
centerTitle: false,
leading: IconButton(
icon: Icon(Icons.close_outlined),
onPressed: () {
_selectedRoomIds.clear();
widget.onCustomAppBar(null);
},
),
title: Text(
L10n.of(context)
.numberSelected(_selectedRoomIds.length.toString()),
),
actions: [
if (_selectedRoomIds.length == 1)
IconButton(
tooltip: L10n.of(context).toggleUnread,
icon: Icon(Matrix.of(context)
.client
.getRoomById(_selectedRoomIds.single)
.isUnread
? Icons.mark_chat_read_outlined
: Icons.mark_chat_unread_outlined),
onPressed: () => _toggleUnread(context),
),
if (_selectedRoomIds.length == 1)
IconButton(
tooltip: L10n.of(context).toggleFavorite,
icon: Icon(Icons.push_pin_outlined),
onPressed: () => _toggleFavouriteRoom(context),
),
if (_selectedRoomIds.length == 1)
IconButton(
icon: Icon(Matrix.of(context)
.client
.getRoomById(_selectedRoomIds.single)
.pushRuleState ==
PushRuleState.notify
? Icons.notifications_off_outlined
: Icons.notifications_outlined),
tooltip: L10n.of(context).toggleMuted,
onPressed: () => _toggleMuted(context),
),
IconButton(
icon: Icon(Icons.archive_outlined),
tooltip: L10n.of(context).archive,
onPressed: () => _archiveAction(context),
),
],
),
);
}
Future<void> _toggleUnread(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return showFutureLoadingDialog(
context: context,
future: () => room.setUnread(!room.isUnread),
);
}
Future<void> _toggleFavouriteRoom(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return showFutureLoadingDialog(
context: context,
future: () => room.setFavourite(!room.isFavourite),
);
}
Future<void> _toggleMuted(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(_selectedRoomIds.single);
return showFutureLoadingDialog(
context: context,
future: () => room.setPushRuleState(
room.pushRuleState == PushRuleState.notify
? PushRuleState.mentions_only
: PushRuleState.notify),
);
}
Future<void> _archiveAction(BuildContext context) async {
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
) ==
OkCancelResult.ok;
if (!confirmed) return;
await showFutureLoadingDialog(
context: context,
future: () => _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();
_toggleSelection(roomId);
}
}
Future<void> waitForFirstSync(BuildContext context) async {
var client = Matrix.of(context).client;
if (client.prevBatch?.isEmpty ?? true) {
await client.onFirstSync.stream.first;
}
return true;
}
@override
Widget build(BuildContext context) {
final selectMode =
_selectedRoomIds.isEmpty ? SelectMode.normal : SelectMode.select;
return Column(children: [
ConnectionStatusHeader(),
Expanded(
child: StreamBuilder(
stream: Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.hasRoomUpdate),
builder: (context, snapshot) {
return FutureBuilder<void>(
future: waitForFirstSync(context),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
var rooms =
List<Room>.from(Matrix.of(context).client.rooms);
rooms.removeWhere((room) =>
room.lastEvent == null ||
(searchMode &&
!room.displayname.toLowerCase().contains(
searchController.text.toLowerCase() ?? '')));
if (rooms.isEmpty && (!searchMode)) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
searchMode
? Icons.search_outlined
: Icons.maps_ugc_outlined,
size: 80,
color: Colors.grey,
),
Center(
child: Text(
searchMode
? L10n.of(context).noRoomsFound
: L10n.of(context).startYourFirstChat,
textAlign: TextAlign.start,
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
}
final totalCount = rooms.length;
return ListView.builder(
controller: _scrollController,
itemCount: totalCount + 1,
padding: EdgeInsets.only(bottom: 24),
itemBuilder: (BuildContext context, int i) => i == 0
? Padding(
padding: EdgeInsets.all(12),
child: DefaultAppBarSearchField(
key: _searchField,
hintText: L10n.of(context).search,
prefixIcon: Icon(Icons.search_outlined),
searchController: searchController,
onChanged: (_) => setState(() => null),
padding: EdgeInsets.zero,
),
)
: ChatListItem(
rooms[i - 1],
selected:
_selectedRoomIds.contains(rooms[i - 1].id),
onTap: selectMode == SelectMode.select &&
widget.onCustomAppBar != null
? () => _toggleSelection(rooms[i - 1].id)
: null,
onLongPress: widget.onCustomAppBar != null
? () => _toggleSelection(rooms[i - 1].id)
: null,
activeChat: widget.activeChat == rooms[i - 1].id,
),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}),
),
]);
}
}

View File

@ -1,154 +0,0 @@
import 'dart:async';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/components/avatar.dart';
import 'package:fluffychat/components/default_app_bar_search_field.dart';
import 'package:fluffychat/components/list_items/contact_list_tile.dart';
import 'package:fluffychat/components/matrix.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:flutter/material.dart';
import '../../app_config.dart';
import '../../utils/client_presence_extension.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ContactList extends StatefulWidget {
final Stream onAppBarButtonTap;
const ContactList({Key key, this.onAppBarButtonTap}) : super(key: key);
@override
_ContactListState createState() => _ContactListState();
}
class _ContactListState extends State<ContactList> {
String _searchQuery = '';
final ScrollController _scrollController = ScrollController();
StreamSubscription _onAppBarButtonTapSub;
StreamSubscription _onSync;
final GlobalKey<DefaultAppBarSearchFieldState> _searchField = GlobalKey();
@override
void initState() {
_onAppBarButtonTapSub =
widget.onAppBarButtonTap.where((i) => i == 0).listen((_) async {
await _scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
WidgetsBinding.instance.addPostFrameCallback(
(_) => _searchField.currentState.requestFocus(),
);
});
super.initState();
}
@override
void dispose() {
_onSync?.cancel();
_onAppBarButtonTapSub?.cancel();
super.dispose();
}
DateTime _lastSetState = DateTime.now();
Timer _coolDown;
void _updateView() {
_lastSetState = DateTime.now();
setState(() => null);
}
@override
Widget build(BuildContext context) {
_onSync ??= Matrix.of(context).client.onSync.stream.listen((_) {
if (DateTime.now().millisecondsSinceEpoch -
_lastSetState.millisecondsSinceEpoch <
1000) {
_coolDown?.cancel();
_coolDown = Timer(Duration(seconds: 1), _updateView);
} else {
_updateView();
}
});
return ListView(
controller: _scrollController,
children: [
Padding(
padding: EdgeInsets.all(12),
child: DefaultAppBarSearchField(
key: _searchField,
hintText: L10n.of(context).search,
prefixIcon: Icon(Icons.search_outlined),
onChanged: (t) => setState(() => _searchQuery = t),
padding: EdgeInsets.zero,
),
),
ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
child: Icon(Icons.add_outlined),
radius: Avatar.defaultSize / 2,
),
title: Text(L10n.of(context).addNewContact),
onTap: () =>
AdaptivePageLayout.of(context).pushNamed('/newprivatechat'),
),
Divider(height: 1),
Builder(builder: (context) {
final contactList = Matrix.of(context)
.client
.contactList
.where((p) =>
p.senderId.toLowerCase().contains(_searchQuery.toLowerCase()))
.toList();
if (contactList.isEmpty) {
return Column(
children: [
SizedBox(height: 32),
Icon(
Icons.people_outlined,
size: 80,
color: Colors.grey,
),
RaisedButton(
elevation: 7,
color: Theme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.share_outlined, color: Colors.white),
SizedBox(width: 16),
Text(
L10n.of(context).inviteContact,
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
onPressed: () => FluffyShare.share(
L10n.of(context).inviteText(
Matrix.of(context).client.userID,
'https://matrix.to/#/${Matrix.of(context).client.userID}'),
context),
),
],
);
}
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 24),
itemCount: contactList.length,
itemBuilder: (context, i) =>
ContactListTile(contact: contactList[i]),
);
}),
],
);
}
}

View File

@ -1,281 +0,0 @@
import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/avatar.dart';
import 'package:fluffychat/components/default_app_bar_search_field.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/components/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../utils/localized_exception_extension.dart';
class Discover extends StatefulWidget {
final String alias;
final String server;
final Stream onAppBarButtonTap;
const Discover({
Key key,
this.alias,
this.server,
this.onAppBarButtonTap,
}) : super(key: key);
@override
_DiscoverState createState() => _DiscoverState();
}
class _DiscoverState extends State<Discover> {
Future<PublicRoomsResponse> _publicRoomsResponse;
String _lastServer;
Timer _coolDown;
String _genericSearchTerm;
final ScrollController _scrollController = ScrollController();
StreamSubscription _onAppBarButtonTapSub;
final GlobalKey<DefaultAppBarSearchFieldState> _searchField = GlobalKey();
@override
void dispose() {
_onAppBarButtonTapSub?.cancel();
super.dispose();
}
void _search(BuildContext context, String query) async {
_coolDown?.cancel();
_coolDown = Timer(
Duration(milliseconds: 500),
() => setState(() {
_genericSearchTerm = query;
_publicRoomsResponse = null;
}),
);
}
Future<String> _joinRoomAndWait(
BuildContext context,
String roomId,
String alias,
) async {
if (Matrix.of(context).client.getRoomById(roomId) != null) {
return roomId;
}
final newRoomId = await Matrix.of(context)
.client
.joinRoomOrAlias(alias?.isNotEmpty ?? false ? alias : roomId);
await Matrix.of(context)
.client
.onRoomUpdate
.stream
.firstWhere((r) => r.id == newRoomId);
return newRoomId;
}
void _joinGroupAction(BuildContext context, PublicRoom room) async {
if (await showOkCancelAlertDialog(
context: context,
okLabel: L10n.of(context).joinRoom,
title: '${room.name} (${room.numJoinedMembers ?? 0})',
message: room.topic ?? L10n.of(context).noDescription,
) ==
OkCancelResult.cancel) {
return;
}
final success = await showFutureLoadingDialog(
context: context,
future: () => _joinRoomAndWait(
context,
room.roomId,
room.canonicalAlias ?? room.aliases.first,
),
);
if (success.error == null) {
await AdaptivePageLayout.of(context)
.pushNamedAndRemoveUntilIsFirst('/rooms/${success.result}');
}
}
@override
void initState() {
_genericSearchTerm = widget.alias;
_onAppBarButtonTapSub =
widget.onAppBarButtonTap.where((i) => i == 2).listen((_) async {
await _scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
WidgetsBinding.instance.addPostFrameCallback(
(_) => _searchField.currentState.requestFocus(),
);
});
super.initState();
}
@override
Widget build(BuildContext context) {
final server = _genericSearchTerm?.isValidMatrixId ?? false
? _genericSearchTerm.domain
: widget.server;
if (_lastServer != server) {
_lastServer = server;
_publicRoomsResponse = null;
}
_publicRoomsResponse ??= Matrix.of(context)
.client
.searchPublicRooms(
server: server,
genericSearchTerm: _genericSearchTerm,
)
.catchError((error) {
if (widget.alias == null) {
throw error;
}
return PublicRoomsResponse.fromJson({
'chunk': [],
});
}).then((PublicRoomsResponse res) {
if (widget.alias != null &&
!res.chunk.any((room) =>
(room.aliases?.contains(widget.alias) ?? false) ||
room.canonicalAlias == widget.alias)) {
// we have to tack on the original alias
res.chunk.add(PublicRoom.fromJson(<String, dynamic>{
'aliases': [widget.alias],
'name': widget.alias,
}));
}
return res;
});
return ListView(
controller: _scrollController,
children: [
Padding(
padding: EdgeInsets.all(12),
child: DefaultAppBarSearchField(
key: _searchField,
hintText: L10n.of(context).search,
prefixIcon: Icon(Icons.search_outlined),
onChanged: (t) => _search(context, t),
padding: EdgeInsets.zero,
),
),
FutureBuilder<PublicRoomsResponse>(
future: _publicRoomsResponse,
builder: (BuildContext context,
AsyncSnapshot<PublicRoomsResponse> snapshot) {
if (snapshot.hasError) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 32),
Icon(
Icons.error_outlined,
size: 80,
color: Colors.grey,
),
Center(
child: Text(
snapshot.error.toLocalizedString(context),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(child: CircularProgressIndicator());
}
final publicRoomsResponse = snapshot.data;
if (publicRoomsResponse.chunk.isEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 32),
Icon(
Icons.search_outlined,
size: 80,
color: Colors.grey,
),
Center(
child: Text(
L10n.of(context).noPublicRoomsFound,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
}
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.all(12),
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: publicRoomsResponse.chunk.length,
itemBuilder: (BuildContext context, int i) => Material(
elevation: 2,
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: () => _joinGroupAction(
context,
publicRoomsResponse.chunk[i],
),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
Uri.parse(
publicRoomsResponse.chunk[i].avatarUrl ?? ''),
publicRoomsResponse.chunk[i].name),
Text(
publicRoomsResponse.chunk[i].name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 1,
textAlign: TextAlign.center,
),
Text(
L10n.of(context).countParticipants(
publicRoomsResponse.chunk[i].numJoinedMembers ??
0),
style: TextStyle(fontSize: 10.5),
maxLines: 1,
textAlign: TextAlign.center,
),
Text(
publicRoomsResponse.chunk[i].topic ??
L10n.of(context).noDescription,
maxLines: 4,
textAlign: TextAlign.center,
),
],
),
),
),
),
);
}),
],
);
}
}

View File

@ -21,16 +21,13 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../components/content_banner.dart';
import '../components/content_banner.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../../components/matrix.dart';
import '../../app_config.dart';
import '../../config/setting_keys.dart';
import '../components/matrix.dart';
import '../app_config.dart';
import '../config/setting_keys.dart';
class Settings extends StatefulWidget {
final Stream onAppBarButtonTap;
const Settings({Key key, this.onAppBarButtonTap}) : super(key: key);
@override
_SettingsState createState() => _SettingsState();
}
@ -42,21 +39,6 @@ class _SettingsState extends State<Settings> {
bool crossSigningCached;
Future<bool> megolmBackupCachedFuture;
bool megolmBackupCached;
StreamSubscription _onAppBarButtonTapSub;
@override
void initState() {
_onAppBarButtonTapSub = widget.onAppBarButtonTap
.where((i) => i == 3)
.listen((_) => logoutAction(context));
super.initState();
}
@override
void dispose() {
_onAppBarButtonTapSub?.cancel();
super.dispose();
}
void logoutAction(BuildContext context) async {
if (await showOkCancelAlertDialog(
@ -343,16 +325,32 @@ class _SettingsState extends State<Settings> {
return c;
});
}
return ListView(
children: <Widget>[
ContentBanner(
profile?.avatarUrl,
height: 200,
opacity: 1,
defaultIcon: Icons.account_circle_outlined,
loading: profile == null,
onEdit: () => setAvatarAction(context),
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) =>
<Widget>[
SliverAppBar(
elevation: Theme.of(context).appBarTheme.elevation,
leading: BackButton(),
expandedHeight: 300.0,
floating: true,
pinned: true,
title: Text(L10n.of(context).settings,
style: TextStyle(
color: Theme.of(context)
.appBarTheme
.textTheme
.headline6
.color)),
backgroundColor: Theme.of(context).appBarTheme.color,
flexibleSpace: FlexibleSpaceBar(
background: ContentBanner(profile?.avatarUrl,
onEdit: () => setAvatarAction(context)),
),
),
],
body: ListView(
children: <Widget>[
ListTile(
title: Text(
L10n.of(context).notifications,
@ -539,6 +537,8 @@ class _SettingsState extends State<Settings> {
onTap: () => PlatformInfos.showDialog(context),
),
],
),
),
);
}
}

View File

@ -1,35 +0,0 @@
import 'dart:async';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/components/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'home_view_parts/chat_list.dart';
class ShareView extends StatelessWidget {
final StreamController<int> _onAppBarButtonTap =
StreamController<int>.broadcast();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.close_outlined),
onPressed: () {
Matrix.of(context).shareContent = null;
AdaptivePageLayout.of(context).pop();
},
),
title: Text(L10n.of(context).share),
actions: [
IconButton(
icon: Icon(Icons.search_outlined),
onPressed: () => _onAppBarButtonTap.add(1),
),
],
),
body: ChatList(onAppBarButtonTap: _onAppBarButtonTap.stream),
);
}
}