mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-04 06:17:26 +01:00 
			
		
		
		
	change: Revert new design
This commit is contained in:
		
							parent
							
								
									7f6222ccbb
								
							
						
					
					
						commit
						ebe246d388
					
				
							
								
								
									
										158
									
								
								lib/components/horizontal_stories_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								lib/components/horizontal_stories_list.dart
									
									
									
									
									
										Normal 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,
 | 
			
		||||
                      )),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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}');
 | 
			
		||||
              });
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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(),
 | 
			
		||||
          );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										486
									
								
								lib/views/chat_list.dart
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										289
									
								
								lib/views/discover.dart
									
									
									
									
									
										Normal 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,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              }),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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(),
 | 
			
		||||
                    );
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
      ),
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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]),
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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,202 +325,220 @@ 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),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).notifications,
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              color: Theme.of(context).accentColor,
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
    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)),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.notifications_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).notifications),
 | 
			
		||||
          onTap: () => AdaptivePageLayout.of(context)
 | 
			
		||||
              .pushNamed('/settings/notifications'),
 | 
			
		||||
        ),
 | 
			
		||||
        Divider(thickness: 1),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).chat,
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              color: Theme.of(context).accentColor,
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(L10n.of(context).changeTheme),
 | 
			
		||||
          onTap: () =>
 | 
			
		||||
              AdaptivePageLayout.of(context).pushNamed('/settings/style'),
 | 
			
		||||
          trailing: Icon(Icons.style_outlined),
 | 
			
		||||
        ),
 | 
			
		||||
        SettingsSwitchListTile(
 | 
			
		||||
          title: L10n.of(context).renderRichContent,
 | 
			
		||||
          onChanged: (b) => AppConfig.renderHtml = b,
 | 
			
		||||
          storeKey: SettingKeys.renderHtml,
 | 
			
		||||
          defaultValue: AppConfig.renderHtml,
 | 
			
		||||
        ),
 | 
			
		||||
        SettingsSwitchListTile(
 | 
			
		||||
          title: L10n.of(context).hideRedactedEvents,
 | 
			
		||||
          onChanged: (b) => AppConfig.hideRedactedEvents = b,
 | 
			
		||||
          storeKey: SettingKeys.hideRedactedEvents,
 | 
			
		||||
          defaultValue: AppConfig.hideRedactedEvents,
 | 
			
		||||
        ),
 | 
			
		||||
        SettingsSwitchListTile(
 | 
			
		||||
          title: L10n.of(context).hideUnknownEvents,
 | 
			
		||||
          onChanged: (b) => AppConfig.hideUnknownEvents = b,
 | 
			
		||||
          storeKey: SettingKeys.hideUnknownEvents,
 | 
			
		||||
          defaultValue: AppConfig.hideUnknownEvents,
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(L10n.of(context).emoteSettings),
 | 
			
		||||
          onTap: () =>
 | 
			
		||||
              AdaptivePageLayout.of(context).pushNamed('/settings/emotes'),
 | 
			
		||||
          trailing: Icon(Icons.insert_emoticon_outlined),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(L10n.of(context).archive),
 | 
			
		||||
          onTap: () => AdaptivePageLayout.of(context).pushNamed('/archive'),
 | 
			
		||||
          trailing: Icon(Icons.archive_outlined),
 | 
			
		||||
        ),
 | 
			
		||||
        Divider(thickness: 1),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).account,
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              color: Theme.of(context).accentColor,
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.edit_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).editDisplayname),
 | 
			
		||||
          subtitle: Text(profile?.displayname ?? client.userID.localpart),
 | 
			
		||||
          onTap: () => setDisplaynameAction(context),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.phone_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).editJitsiInstance),
 | 
			
		||||
          subtitle: Text(AppConfig.jitsiInstance),
 | 
			
		||||
          onTap: () => setJitsiInstanceAction(context),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.devices_other_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).devices),
 | 
			
		||||
          onTap: () =>
 | 
			
		||||
              AdaptivePageLayout.of(context).pushNamed('/settings/devices'),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.block_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).ignoredUsers),
 | 
			
		||||
          onTap: () =>
 | 
			
		||||
              AdaptivePageLayout.of(context).pushNamed('/settings/ignore'),
 | 
			
		||||
        ),
 | 
			
		||||
        SentrySwitchListTile(),
 | 
			
		||||
        Divider(thickness: 1),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.security_outlined),
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).changePassword,
 | 
			
		||||
          ),
 | 
			
		||||
          onTap: () => _changePasswordAccountAction(context),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.email_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).passwordRecovery),
 | 
			
		||||
          onTap: () =>
 | 
			
		||||
              AdaptivePageLayout.of(context).pushNamed('/settings/3pid'),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.exit_to_app_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).logout),
 | 
			
		||||
          onTap: () => logoutAction(context),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.delete_forever_outlined),
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).deleteAccount,
 | 
			
		||||
            style: TextStyle(color: Colors.red),
 | 
			
		||||
          ),
 | 
			
		||||
          onTap: () => _deleteAccountAction(context),
 | 
			
		||||
        ),
 | 
			
		||||
        if (client.encryption != null) ...{
 | 
			
		||||
          Divider(thickness: 1),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            title: Text(
 | 
			
		||||
              L10n.of(context).security,
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                color: Theme.of(context).accentColor,
 | 
			
		||||
                fontWeight: FontWeight.bold,
 | 
			
		||||
        ],
 | 
			
		||||
        body: ListView(
 | 
			
		||||
          children: <Widget>[
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(
 | 
			
		||||
                L10n.of(context).notifications,
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  color: Theme.of(context).accentColor,
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          if (PlatformInfos.isMobile)
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.lock_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).appLock),
 | 
			
		||||
              onTap: () => _setAppLockAction(context),
 | 
			
		||||
              trailing: Icon(Icons.notifications_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).notifications),
 | 
			
		||||
              onTap: () => AdaptivePageLayout.of(context)
 | 
			
		||||
                  .pushNamed('/settings/notifications'),
 | 
			
		||||
            ),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            title: Text(L10n.of(context).yourPublicKey),
 | 
			
		||||
            onTap: () => showOkAlertDialog(
 | 
			
		||||
              context: context,
 | 
			
		||||
              title: L10n.of(context).yourPublicKey,
 | 
			
		||||
              message: client.fingerprintKey.beautified,
 | 
			
		||||
            Divider(thickness: 1),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(
 | 
			
		||||
                L10n.of(context).chat,
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  color: Theme.of(context).accentColor,
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            trailing: Icon(Icons.vpn_key_outlined),
 | 
			
		||||
          ),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            title: Text(L10n.of(context).cachedKeys),
 | 
			
		||||
            trailing: Icon(Icons.wb_cloudy_outlined),
 | 
			
		||||
            subtitle: Text(
 | 
			
		||||
                '${client.encryption.keyManager.enabled ? L10n.of(context).onlineKeyBackupEnabled : L10n.of(context).onlineKeyBackupDisabled}\n${client.encryption.crossSigning.enabled ? L10n.of(context).crossSigningEnabled : L10n.of(context).crossSigningDisabled}'),
 | 
			
		||||
            onTap: () => BootstrapDialog(
 | 
			
		||||
              l10n: L10n.of(context),
 | 
			
		||||
              client: Matrix.of(context).client,
 | 
			
		||||
            ).show(context),
 | 
			
		||||
          ),
 | 
			
		||||
        },
 | 
			
		||||
        Divider(thickness: 1),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text(
 | 
			
		||||
            L10n.of(context).about,
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              color: Theme.of(context).accentColor,
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(L10n.of(context).changeTheme),
 | 
			
		||||
              onTap: () =>
 | 
			
		||||
                  AdaptivePageLayout.of(context).pushNamed('/settings/style'),
 | 
			
		||||
              trailing: Icon(Icons.style_outlined),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          onTap: () => AdaptivePageLayout.of(context).pushNamed('/logs'),
 | 
			
		||||
            SettingsSwitchListTile(
 | 
			
		||||
              title: L10n.of(context).renderRichContent,
 | 
			
		||||
              onChanged: (b) => AppConfig.renderHtml = b,
 | 
			
		||||
              storeKey: SettingKeys.renderHtml,
 | 
			
		||||
              defaultValue: AppConfig.renderHtml,
 | 
			
		||||
            ),
 | 
			
		||||
            SettingsSwitchListTile(
 | 
			
		||||
              title: L10n.of(context).hideRedactedEvents,
 | 
			
		||||
              onChanged: (b) => AppConfig.hideRedactedEvents = b,
 | 
			
		||||
              storeKey: SettingKeys.hideRedactedEvents,
 | 
			
		||||
              defaultValue: AppConfig.hideRedactedEvents,
 | 
			
		||||
            ),
 | 
			
		||||
            SettingsSwitchListTile(
 | 
			
		||||
              title: L10n.of(context).hideUnknownEvents,
 | 
			
		||||
              onChanged: (b) => AppConfig.hideUnknownEvents = b,
 | 
			
		||||
              storeKey: SettingKeys.hideUnknownEvents,
 | 
			
		||||
              defaultValue: AppConfig.hideUnknownEvents,
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(L10n.of(context).emoteSettings),
 | 
			
		||||
              onTap: () =>
 | 
			
		||||
                  AdaptivePageLayout.of(context).pushNamed('/settings/emotes'),
 | 
			
		||||
              trailing: Icon(Icons.insert_emoticon_outlined),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(L10n.of(context).archive),
 | 
			
		||||
              onTap: () => AdaptivePageLayout.of(context).pushNamed('/archive'),
 | 
			
		||||
              trailing: Icon(Icons.archive_outlined),
 | 
			
		||||
            ),
 | 
			
		||||
            Divider(thickness: 1),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(
 | 
			
		||||
                L10n.of(context).account,
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  color: Theme.of(context).accentColor,
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.edit_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).editDisplayname),
 | 
			
		||||
              subtitle: Text(profile?.displayname ?? client.userID.localpart),
 | 
			
		||||
              onTap: () => setDisplaynameAction(context),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.phone_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).editJitsiInstance),
 | 
			
		||||
              subtitle: Text(AppConfig.jitsiInstance),
 | 
			
		||||
              onTap: () => setJitsiInstanceAction(context),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.devices_other_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).devices),
 | 
			
		||||
              onTap: () =>
 | 
			
		||||
                  AdaptivePageLayout.of(context).pushNamed('/settings/devices'),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.block_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).ignoredUsers),
 | 
			
		||||
              onTap: () =>
 | 
			
		||||
                  AdaptivePageLayout.of(context).pushNamed('/settings/ignore'),
 | 
			
		||||
            ),
 | 
			
		||||
            SentrySwitchListTile(),
 | 
			
		||||
            Divider(thickness: 1),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.security_outlined),
 | 
			
		||||
              title: Text(
 | 
			
		||||
                L10n.of(context).changePassword,
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () => _changePasswordAccountAction(context),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.email_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).passwordRecovery),
 | 
			
		||||
              onTap: () =>
 | 
			
		||||
                  AdaptivePageLayout.of(context).pushNamed('/settings/3pid'),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.exit_to_app_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).logout),
 | 
			
		||||
              onTap: () => logoutAction(context),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.delete_forever_outlined),
 | 
			
		||||
              title: Text(
 | 
			
		||||
                L10n.of(context).deleteAccount,
 | 
			
		||||
                style: TextStyle(color: Colors.red),
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () => _deleteAccountAction(context),
 | 
			
		||||
            ),
 | 
			
		||||
            if (client.encryption != null) ...{
 | 
			
		||||
              Divider(thickness: 1),
 | 
			
		||||
              ListTile(
 | 
			
		||||
                title: Text(
 | 
			
		||||
                  L10n.of(context).security,
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                    color: Theme.of(context).accentColor,
 | 
			
		||||
                    fontWeight: FontWeight.bold,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              if (PlatformInfos.isMobile)
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  trailing: Icon(Icons.lock_outlined),
 | 
			
		||||
                  title: Text(L10n.of(context).appLock),
 | 
			
		||||
                  onTap: () => _setAppLockAction(context),
 | 
			
		||||
                ),
 | 
			
		||||
              ListTile(
 | 
			
		||||
                title: Text(L10n.of(context).yourPublicKey),
 | 
			
		||||
                onTap: () => showOkAlertDialog(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  title: L10n.of(context).yourPublicKey,
 | 
			
		||||
                  message: client.fingerprintKey.beautified,
 | 
			
		||||
                ),
 | 
			
		||||
                trailing: Icon(Icons.vpn_key_outlined),
 | 
			
		||||
              ),
 | 
			
		||||
              ListTile(
 | 
			
		||||
                title: Text(L10n.of(context).cachedKeys),
 | 
			
		||||
                trailing: Icon(Icons.wb_cloudy_outlined),
 | 
			
		||||
                subtitle: Text(
 | 
			
		||||
                    '${client.encryption.keyManager.enabled ? L10n.of(context).onlineKeyBackupEnabled : L10n.of(context).onlineKeyBackupDisabled}\n${client.encryption.crossSigning.enabled ? L10n.of(context).crossSigningEnabled : L10n.of(context).crossSigningDisabled}'),
 | 
			
		||||
                onTap: () => BootstrapDialog(
 | 
			
		||||
                  l10n: L10n.of(context),
 | 
			
		||||
                  client: Matrix.of(context).client,
 | 
			
		||||
                ).show(context),
 | 
			
		||||
              ),
 | 
			
		||||
            },
 | 
			
		||||
            Divider(thickness: 1),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text(
 | 
			
		||||
                L10n.of(context).about,
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  color: Theme.of(context).accentColor,
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () => AdaptivePageLayout.of(context).pushNamed('/logs'),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.help_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).help),
 | 
			
		||||
              onTap: () => launch(AppConfig.supportUrl),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.privacy_tip_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).privacy),
 | 
			
		||||
              onTap: () => launch(AppConfig.privacyUrl),
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              trailing: Icon(Icons.link_outlined),
 | 
			
		||||
              title: Text(L10n.of(context).about),
 | 
			
		||||
              onTap: () => PlatformInfos.showDialog(context),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.help_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).help),
 | 
			
		||||
          onTap: () => launch(AppConfig.supportUrl),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.privacy_tip_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).privacy),
 | 
			
		||||
          onTap: () => launch(AppConfig.privacyUrl),
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          trailing: Icon(Icons.link_outlined),
 | 
			
		||||
          title: Text(L10n.of(context).about),
 | 
			
		||||
          onTap: () => PlatformInfos.showDialog(context),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user