Browse Source

Settings redesign (#4)

* Stashed settings redesign work

* Move settings views to the separate folder

* Move theme settings

* Finish moving around the settings

* Fix settings navigation on wide screens

* Give visual hints of selected settings view
yiffed
Inex Code 8 months ago
committed by GitHub
parent
commit
f956476bf3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 85
      lib/components/settings_themes.dart
  2. 15
      lib/l10n/intl_en.arb
  3. 6
      lib/views/chat_details.dart
  4. 608
      lib/views/settings.dart
  5. 4
      lib/views/settings/app_info.dart
  6. 226
      lib/views/settings/settings_account.dart
  7. 49
      lib/views/settings/settings_chat.dart
  8. 12
      lib/views/settings/settings_devices.dart
  9. 10
      lib/views/settings/settings_emotes.dart
  10. 234
      lib/views/settings/settings_encryption.dart
  11. 44
      lib/views/settings/settings_homeserver.dart
  12. 6
      lib/views/settings/settings_ignore_list.dart
  13. 8
      lib/views/settings/settings_multiple_emotes.dart
  14. 155
      lib/views/settings/settings_themes.dart

85
lib/components/settings_themes.dart

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../components/matrix.dart';
import '../components/theme_switcher.dart';
class ThemesSettings extends StatefulWidget {
@override
ThemesSettingsState createState() => ThemesSettingsState();
}
class ThemesSettingsState extends State<ThemesSettings> {
Themes _selectedTheme;
bool _amoledEnabled;
@override
Widget build(BuildContext context) {
final matrix = Matrix.of(context);
final themeEngine = ThemeSwitcherWidget.of(context);
_selectedTheme = themeEngine.selectedTheme;
_amoledEnabled = themeEngine.amoledEnabled;
return Column(
children: <Widget>[
RadioListTile<Themes>(
title: Text(
L10n.of(context).systemTheme,
),
value: Themes.system,
groupValue: _selectedTheme,
activeColor: Theme.of(context).primaryColor,
onChanged: (Themes value) {
setState(() {
_selectedTheme = value;
themeEngine.switchTheme(matrix, value, _amoledEnabled);
});
},
),
RadioListTile<Themes>(
title: Text(
L10n.of(context).lightTheme,
),
value: Themes.light,
groupValue: _selectedTheme,
activeColor: Theme.of(context).primaryColor,
onChanged: (Themes value) {
setState(() {
_selectedTheme = value;
themeEngine.switchTheme(matrix, value, _amoledEnabled);
});
},
),
RadioListTile<Themes>(
title: Text(
L10n.of(context).darkTheme,
),
value: Themes.dark,
groupValue: _selectedTheme,
activeColor: Theme.of(context).primaryColor,
onChanged: (Themes value) {
setState(() {
_selectedTheme = value;
themeEngine.switchTheme(matrix, value, _amoledEnabled);
});
},
),
ListTile(
title: Text(
L10n.of(context).useAmoledTheme,
),
trailing: Switch(
value: _amoledEnabled,
activeColor: Theme.of(context).primaryColor,
onChanged: (bool value) {
setState(() {
_amoledEnabled = value;
themeEngine.switchTheme(matrix, _selectedTheme, value);
});
},
),
),
],
);
}
}

15
lib/l10n/intl_en.arb

@ -114,6 +114,11 @@
"type": "text",
"placeholders": {}
},
"avatar": "Avatar",
"@avatar": {
"type": "text",
"placeholders": {}
},
"avatarHasBeenChanged": "Avatar has been changed",
"@avatarHasBeenChanged": {
"type": "text",
@ -207,6 +212,11 @@
"type": "text",
"placeholders": {}
},
"changeThePassword": "Change the password",
"@changeThePassword": {
"type": "text",
"placeholders": {}
},
"changedTheGuestAccessRules": "{username} changed the guest access rules",
"@changedTheGuestAccessRules": {
"type": "text",
@ -681,6 +691,11 @@
"type": "text",
"placeholders": {}
},
"homeserver": "Homeserver",
"@homeserver": {
"type": "text",
"placeholders": {}
},
"homeserverIsNotCompatible": "Homeserver is not compatible",
"@homeserverIsNotCompatible": {
"type": "text",

6
lib/views/chat_details.dart

@ -20,9 +20,9 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix_link_text/link_text.dart';
import './settings_emotes.dart';
import './settings_multiple_emotes.dart';
import '../utils/url_launcher.dart';
import 'package:furrychat/views/settings/settings_emotes.dart';
import 'package:furrychat/views/settings/settings_multiple_emotes.dart';
import 'package:furrychat/utils/url_launcher.dart';
class ChatDetails extends StatefulWidget {
final Room room;

608
lib/views/settings.dart

@ -1,41 +1,53 @@
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:furrychat/components/settings_themes.dart';
import 'package:furrychat/config/app_config.dart';
import 'package:furrychat/utils/platform_infos.dart';
import 'package:furrychat/views/settings_devices.dart';
import 'package:furrychat/views/settings_ignore_list.dart';
import 'package:flutter/foundation.dart';
import 'package:furrychat/views/settings/settings_account.dart';
import 'package:furrychat/views/settings/settings_chat.dart';
import 'package:furrychat/views/settings/settings_devices.dart';
import 'package:furrychat/views/settings/settings_encryption.dart';
import 'package:furrychat/views/settings/settings_homeserver.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart';
import 'package:furrychat/views/settings/settings_themes.dart';
import 'package:url_launcher/url_launcher.dart';
import '../components/adaptive_page_layout.dart';
import '../components/content_banner.dart';
import '../components/dialogs/simple_dialogs.dart';
import '../components/matrix.dart';
import '../utils/app_route.dart';
import 'app_info.dart';
import 'chat_list.dart';
import 'settings_emotes.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/utils/app_route.dart';
import 'package:furrychat/views/settings/settings_emotes.dart';
enum SettingsViews {
account,
homeserver,
themes,
chat,
emotes,
encryption,
devices,
}
class SettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
secondScaffold: Settings(),
primaryPage: FocusPage.FIRST,
firstScaffold: Settings(
isInFocus: true,
),
secondScaffold: Scaffold(
body: Center(
child: Image.asset('assets/logo.png', width: 100, height: 100),
),
),
);
}
}
class Settings extends StatefulWidget {
final bool isInFocus;
final SettingsViews currentSetting;
const Settings({this.isInFocus = false, this.currentSetting, Key key})
: super(key: key);
@override
_SettingsState createState() => _SettingsState();
}
@ -43,196 +55,21 @@ class Settings extends StatefulWidget {
class _SettingsState extends State<Settings> {
Future<dynamic> profileFuture;
dynamic profile;
Future<bool> crossSigningCachedFuture;
bool crossSigningCached;
Future<bool> megolmBackupCachedFuture;
bool megolmBackupCached;
void logoutAction(BuildContext context) async {
if (await SimpleDialogs(context).askConfirmation() == false) {
return;
}
var matrix = Matrix.of(context);
await SimpleDialogs(context)
.tryRequestWithLoadingDialog(matrix.client.logout());
}
void _changePasswordAccountAction(BuildContext context) async {
final oldPassword = await SimpleDialogs(context).enterText(
password: true,
titleText: L10n.of(context).pleaseEnterYourPassword,
);
if (oldPassword == null) return;
final newPassword = await SimpleDialogs(context).enterText(
password: true,
titleText: L10n.of(context).chooseAStrongPassword,
);
if (newPassword == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog(
Matrix.of(context)
.client
.changePassword(newPassword, oldPassword: oldPassword),
);
BotToast.showText(text: L10n.of(context).passwordHasBeenChanged);
}
void _deleteAccountAction(BuildContext context) async {
if (await SimpleDialogs(context).askConfirmation(
titleText: L10n.of(context).warning,
contentText: L10n.of(context).deactivateAccountWarning,
dangerous: true,
) ==
false) {
return;
}
if (await SimpleDialogs(context).askConfirmation(dangerous: true) ==
false) {
return;
}
final password = await SimpleDialogs(context).enterText(
password: true,
titleText: L10n.of(context).pleaseEnterYourPassword,
);
if (password == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog(
Matrix.of(context).client.deactivateAccount(auth: {
'type': 'm.login.password',
'user': Matrix.of(context).client.userID,
'password': password,
}),
);
}
void setJitsiInstanceAction(BuildContext context) async {
var jitsi = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).editJitsiInstance,
hintText: Matrix.of(context).jitsiInstance,
labelText: L10n.of(context).editJitsiInstance,
);
if (jitsi == null) return;
if (!jitsi.endsWith('/')) {
jitsi += '/';
}
final matrix = Matrix.of(context);
await matrix.store.setItem('chat.fluffy.jitsi_instance', jitsi);
matrix.jitsiInstance = jitsi;
}
void setDisplaynameAction(BuildContext context) async {
final displayname = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).editDisplayname,
hintText:
profile?.displayname ?? Matrix.of(context).client.userID.localpart,
labelText: L10n.of(context).enterAUsername,
);
if (displayname == null) return;
final matrix = Matrix.of(context);
final success = await SimpleDialogs(context).tryRequestWithLoadingDialog(
matrix.client.setDisplayname(matrix.client.userID, displayname),
);
if (success != false) {
setState(() {
profileFuture = null;
profile = null;
});
}
}
void setAvatarAction(BuildContext context) async {
MatrixFile file;
if (PlatformInfos.isMobile) {
final result = await ImagePicker().getImage(
source: ImageSource.gallery,
imageQuality: 50,
maxWidth: 1600,
maxHeight: 1600);
if (result == null) return;
file = MatrixFile(
bytes: await result.readAsBytes(),
name: result.path,
);
} else {
final result =
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
if (result == null) return;
file = MatrixFile(
bytes: result.toUint8List(),
name: result.fileName,
);
}
final matrix = Matrix.of(context);
final success = await SimpleDialogs(context).tryRequestWithLoadingDialog(
matrix.client.setAvatar(file),
);
if (success != false) {
setState(() {
profileFuture = null;
profile = null;
});
}
}
void setWallpaperAction(BuildContext context) async {
final wallpaper = await ImagePicker().getImage(source: ImageSource.gallery);
if (wallpaper == null) return;
Matrix.of(context).wallpaper = File(wallpaper.path);
await Matrix.of(context)
.store
.setItem('chat.fluffy.wallpaper', wallpaper.path);
setState(() => null);
}
void deleteWallpaperAction(BuildContext context) async {
Matrix.of(context).wallpaper = null;
await Matrix.of(context).store.setItem('chat.fluffy.wallpaper', null);
setState(() => null);
}
Future<void> requestSSSSCache(BuildContext context) async {
final handle = Matrix.of(context).client.encryption.ssss.open();
final str = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).askSSSSCache,
hintText: L10n.of(context).passphraseOrKey,
password: true,
);
if (str != null) {
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
handle.unlock(recoveryKey: str);
valid = true;
} catch (e, s) {
debugPrint('Couldn\'t use recovery key: ' + e.toString());
debugPrint(s.toString());
try {
handle.unlock(passphrase: str);
valid = true;
} catch (e, s) {
debugPrint('Couldn\'t use recovery passphrase: ' + e.toString());
debugPrint(s.toString());
valid = false;
}
}
await Navigator.of(context)?.pop();
if (valid) {
await handle.maybeCacheAll();
await SimpleDialogs(context).inform(
contentText: L10n.of(context).cachedKeys,
);
setState(() {
crossSigningCachedFuture = null;
crossSigningCached = null;
megolmBackupCachedFuture = null;
megolmBackupCached = null;
});
} else {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
}
void _handleTap(Widget child) {
widget.isInFocus
? Navigator.of(context).push(
AppRoute.defaultRoute(
context,
child,
),
)
: Navigator.of(context).pushReplacement(
AppRoute.defaultRoute(
context,
child,
),
);
}
@override
@ -242,323 +79,120 @@ class _SettingsState extends State<Settings> {
if (mounted) setState(() => profile = p);
return p;
});
crossSigningCachedFuture ??=
client.encryption.crossSigning.isCached().then((c) {
if (mounted) setState(() => crossSigningCached = c);
return c;
});
megolmBackupCachedFuture ??=
client.encryption.keyManager.isCached().then((c) {
if (mounted) setState(() => megolmBackupCached = c);
return c;
});
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) =>
<Widget>[
SliverAppBar(
expandedHeight: 300.0,
leading: !widget.isInFocus
? IconButton(
icon: Icon(Icons.close_outlined),
onPressed: () =>
{Navigator.of(context).popUntil((r) => r.isFirst)},
)
: null,
floating: true,
pinned: true,
backgroundColor: Theme.of(context).appBarTheme.color,
flexibleSpace: FlexibleSpaceBar(
title: Text(
L10n.of(context).settings,
style: TextStyle(
color: Theme.of(context)
.appBarTheme
.textTheme
.headline6
.color),
),
background: ContentBanner(
profile?.avatarUrl,
height: 300,
defaultIcon: Icons.account_circle,
loading: profile == null,
onEdit: () => setAvatarAction(context),
),
),
title: Text(L10n.of(context).settings),
),
],
body: ListView(
children: <Widget>[
ListTile(
title: Text(
L10n.of(context).changeTheme,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
ThemesSettings(),
if (!kIsWeb && Matrix.of(context).store != null)
Divider(thickness: 1),
if (!kIsWeb && Matrix.of(context).store != null)
ListTile(
title: Text(
L10n.of(context).wallpaper,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
if (Matrix.of(context).wallpaper != null)
ListTile(
title: Image.file(
Matrix.of(context).wallpaper,
height: 38,
fit: BoxFit.cover,
),
trailing: Icon(
Icons.delete_forever,
color: Colors.red,
),
onTap: () => deleteWallpaperAction(context),
),
if (!kIsWeb && Matrix.of(context).store != null)
Builder(builder: (context) {
return ListTile(
title: Text(L10n.of(context).changeWallpaper),
trailing: Icon(Icons.wallpaper),
onTap: () => setWallpaperAction(context),
);
}),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).chat,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
ListTile(
title: Text(L10n.of(context).renderRichContent),
trailing: Switch(
value: Matrix.of(context).renderHtml,
activeColor: Theme.of(context).primaryColor,
onChanged: (bool newValue) async {
Matrix.of(context).renderHtml = newValue;
await Matrix.of(context)
.store
.setItem('chat.fluffy.renderHtml', newValue ? '1' : '0');
setState(() => null);
},
leading: Icon(
Icons.person_outlined,
),
title: Text(profile?.displayname ?? L10n.of(context).account),
selected:
widget.currentSetting == SettingsViews.account ? true : false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
subtitle: Text(client.userID),
onTap: () => _handleTap(AccountSettingsView()),
),
ListTile(
title: Text(L10n.of(context).emoteSettings),
onTap: () async => await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
EmotesSettingsView(),
),
leading: Icon(
Icons.dns_outlined,
),
trailing: Icon(Icons.insert_emoticon),
title: Text(L10n.of(context).homeserver),
selected: widget.currentSetting == SettingsViews.homeserver
? true
: false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
subtitle: Text(client.homeserver.host),
onTap: () => _handleTap(HomeserverSettingsView()),
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).account,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
leading: Icon(
Icons.color_lens_outlined,
),
title: Text(L10n.of(context).changeTheme),
selected:
widget.currentSetting == SettingsViews.themes ? true : false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
onTap: () => _handleTap(ThemesSettingsView()),
),
ListTile(
trailing: Icon(Icons.edit),
title: Text(L10n.of(context).editDisplayname),
subtitle: Text(profile?.displayname ?? client.userID.localpart),
onTap: () => setDisplaynameAction(context),
),
ListTile(
trailing: Icon(Icons.phone),
title: Text(L10n.of(context).editJitsiInstance),
subtitle: Text(Matrix.of(context).jitsiInstance),
onTap: () => setJitsiInstanceAction(context),
),
ListTile(
trailing: Icon(Icons.devices_other),
title: Text(L10n.of(context).devices),
onTap: () async => await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
DevicesSettingsView(),
),
leading: Icon(
Icons.chat_outlined,
),
title: Text(L10n.of(context).chat),
selected:
widget.currentSetting == SettingsViews.chat ? true : false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
onTap: () => _handleTap(ChatSettingsView()),
),
ListTile(
trailing: Icon(Icons.block),
title: Text(L10n.of(context).ignoredUsers),
onTap: () async => await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
SettingsIgnoreListView(),
),
leading: Icon(
Icons.insert_emoticon_outlined,
),
title: Text(L10n.of(context).emoteSettings),
selected:
widget.currentSetting == SettingsViews.emotes ? true : false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
onTap: () => _handleTap(EmotesSettingsView()),
),
ListTile(
trailing: Icon(Icons.account_circle),
title: Text(L10n.of(context).accountInformation),
onTap: () => Navigator.of(context).push(
AppRoute.defaultRoute(
context,
AppInfoView(),
),
),
),
Divider(thickness: 1),
ListTile(
trailing: Icon(Icons.vpn_key),
title: Text(
'Change password',
),
onTap: () => _changePasswordAccountAction(context),
),
ListTile(
trailing: Icon(Icons.exit_to_app),
title: Text(L10n.of(context).logout),
onTap: () => logoutAction(context),
),
ListTile(
trailing: Icon(Icons.delete_forever),
title: Text(
L10n.of(context).deleteAccount,
style: TextStyle(color: Colors.red),
leading: Icon(
Icons.lock_outline,
),
onTap: () => _deleteAccountAction(context),
title: Text(L10n.of(context).encryption),
selected: widget.currentSetting == SettingsViews.encryption
? true
: false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
onTap: () => _handleTap(EncryptionSettingsView()),
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).encryption,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
leading: Icon(
Icons.devices_other_outlined,
),
),
ListTile(
trailing: Icon(Icons.compare_arrows),
title: Text(client.encryption.crossSigning.enabled
? L10n.of(context).crossSigningEnabled
: L10n.of(context).crossSigningDisabled),
subtitle: client.encryption.crossSigning.enabled
? Text(client.isUnknownSession
? L10n.of(context).unknownSessionVerify
: L10n.of(context).sessionVerified +
', ' +
(crossSigningCached == null
? ''
: (crossSigningCached
? L10n.of(context).keysCached
: L10n.of(context).keysMissing)))
: null,
onTap: () async {
if (!client.encryption.crossSigning.enabled) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).noCrossSignBootstrap,
);
return;
}
if (client.isUnknownSession) {
final str = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).askSSSSVerify,
hintText: L10n.of(context).passphraseOrKey,
password: true,
);
if (str != null) {
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
await client.encryption.crossSigning
.selfSign(recoveryKey: str);
valid = true;
} catch (_) {
try {
await client.encryption.crossSigning
.selfSign(passphrase: str);
valid = true;
} catch (_) {
valid = false;
}
}
await Navigator.of(context)?.pop();
if (valid) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).verifiedSession,
);
setState(() {
crossSigningCachedFuture = null;
crossSigningCached = null;
megolmBackupCachedFuture = null;
megolmBackupCached = null;
});
} else {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
}
}
if (!(await client.encryption.crossSigning.isCached())) {
await requestSSSSCache(context);
}
},
),
ListTile(
trailing: Icon(Icons.wb_cloudy),
title: Text(client.encryption.keyManager.enabled
? L10n.of(context).onlineKeyBackupEnabled
: L10n.of(context).onlineKeyBackupDisabled),
subtitle: client.encryption.keyManager.enabled
? Text(megolmBackupCached == null
? ''
: (megolmBackupCached
? L10n.of(context).keysCached
: L10n.of(context).keysMissing))
: null,
onTap: () async {
if (!client.encryption.keyManager.enabled) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).noMegolmBootstrap,
);
return;
}
if (!(await client.encryption.keyManager.isCached())) {
await requestSSSSCache(context);
}
},
title: Text(L10n.of(context).devices),
selected:
widget.currentSetting == SettingsViews.devices ? true : false,
selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
onTap: () => _handleTap(DevicesSettingsView()),
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).about,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
leading: Icon(
Icons.help_outline_outlined,
),
),
ListTile(
trailing: Icon(Icons.help),
title: Text(L10n.of(context).help),
onTap: () => launch(AppConfig.supportUrl),
),
ListTile(
trailing: Icon(Icons.privacy_tip_rounded),
leading: Icon(
Icons.privacy_tip_outlined,
),
title: Text(L10n.of(context).privacy),
onTap: () => launch(AppConfig.privacyUrl),
),
ListTile(
trailing: Icon(Icons.link),
leading: Icon(
Icons.link_outlined,
),
title: Text(L10n.of(context).license),
onTap: () => showLicensePage(
context: context,
@ -568,7 +202,9 @@ class _SettingsState extends State<Settings> {
),
),
ListTile(
trailing: Icon(Icons.code),
leading: Icon(
Icons.code_outlined,
),
title: Text(L10n.of(context).sourceCode),
onTap: () => launch(AppConfig.sourceCodeUrl),
),

4
lib/views/app_info.dart → lib/views/settings/app_info.dart

@ -1,7 +1,7 @@
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/utils/beautify_string_extension.dart';
import 'package:furrychat/views/chat_list.dart';
import 'package:furrychat/views/settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:olm/olm.dart' as olm;
@ -11,7 +11,7 @@ class AppInfoView extends StatelessWidget {
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
firstScaffold: Settings(),
secondScaffold: AppInfo(),
);
}

226
lib/views/settings/settings_account.dart

@ -0,0 +1,226 @@
import 'package:bot_toast/bot_toast.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:furrychat/components/avatar.dart';
import 'package:furrychat/utils/app_route.dart';
import 'package:furrychat/utils/platform_infos.dart';
import 'package:furrychat/components/dialogs/simple_dialogs.dart';
import 'package:furrychat/views/settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/views/settings/settings_ignore_list.dart';
import 'package:image_picker/image_picker.dart';
class AccountSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: Settings(currentSetting: SettingsViews.account),
secondScaffold: AccountSettings(),
);
}
}
class AccountSettings extends StatefulWidget {
@override
_AccountSettingsState createState() => _AccountSettingsState();
}
class _AccountSettingsState extends State<AccountSettings> {
Future<dynamic> profileFuture;
dynamic profile;
void logoutAction(BuildContext context) async {
if (await SimpleDialogs(context).askConfirmation() == false) {
return;
}
var matrix = Matrix.of(context);
await SimpleDialogs(context)
.tryRequestWithLoadingDialog(matrix.client.logout());
}
void _changePasswordAccountAction(BuildContext context) async {
final oldPassword = await SimpleDialogs(context).enterText(
password: true,
titleText: L10n.of(context).pleaseEnterYourPassword,
);
if (oldPassword == null) return;
final newPassword = await SimpleDialogs(context).enterText(
password: true,
titleText: L10n.of(context).chooseAStrongPassword,
);
if (newPassword == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog(
Matrix.of(context)
.client
.changePassword(newPassword, oldPassword: oldPassword),
);
BotToast.showText(text: L10n.of(context).passwordHasBeenChanged);
}
void _deleteAccountAction(BuildContext context) async {
if (await SimpleDialogs(context).askConfirmation(
titleText: L10n.of(context).warning,
contentText: L10n.of(context).deactivateAccountWarning,
dangerous: true,
) ==
false) {
return;
}
if (await SimpleDialogs(context).askConfirmation(dangerous: true) ==
false) {
return;
}
final password = await SimpleDialogs(context).enterText(
password: true,
titleText: L10n.of(context).pleaseEnterYourPassword,
);
if (password == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog(
Matrix.of(context).client.deactivateAccount(auth: {
'type': 'm.login.password',
'user': Matrix.of(context).client.userID,
'password': password,
}),
);
}
void setJitsiInstanceAction(BuildContext context) async {
var jitsi = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).editJitsiInstance,
hintText: Matrix.of(context).jitsiInstance,
labelText: L10n.of(context).editJitsiInstance,
);
if (jitsi == null) return;
if (!jitsi.endsWith('/')) {
jitsi += '/';
}
final matrix = Matrix.of(context);
await matrix.store.setItem('chat.fluffy.jitsi_instance', jitsi);
matrix.jitsiInstance = jitsi;
}
void setDisplaynameAction(BuildContext context) async {
final displayname = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).editDisplayname,
hintText:
profile?.displayname ?? Matrix.of(context).client.userID.localpart,
labelText: L10n.of(context).enterAUsername,
);
if (displayname == null) return;
final matrix = Matrix.of(context);
final success = await SimpleDialogs(context).tryRequestWithLoadingDialog(
matrix.client.setDisplayname(matrix.client.userID, displayname),
);
if (success != false) {
setState(() {
profileFuture = null;
profile = null;
});
}
}
void setAvatarAction(BuildContext context) async {
MatrixFile file;
if (PlatformInfos.isMobile) {
final result = await ImagePicker().getImage(
source: ImageSource.gallery,
imageQuality: 50,
maxWidth: 1600,
maxHeight: 1600);
if (result == null) return;
file = MatrixFile(
bytes: await result.readAsBytes(),
name: result.path,
);
} else {
final result =
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
if (result == null) return;
file = MatrixFile(
bytes: result.toUint8List(),
name: result.fileName,
);
}
final matrix = Matrix.of(context);
final success = await SimpleDialogs(context).tryRequestWithLoadingDialog(
matrix.client.setAvatar(file),
);
if (success != false) {
setState(() {
profileFuture = null;
profile = null;
});
}
}
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
profileFuture ??= client.ownProfile.then((p) {
if (mounted) setState(() => profile = p);
return p;
});
return Scaffold(
appBar: AppBar(title: Text(L10n.of(context).account)),
body: ListView(
children: [
ListTile(
leading: Avatar(
profile?.avatarUrl,
profile?.displayname ?? client.userID.toString(),
size: 24.0,
),
title: Text(L10n.of(context).avatar),
onTap: () => setAvatarAction(context),
),
ListTile(
leading: Icon(Icons.edit_outlined),
title: Text(L10n.of(context).editDisplayname),
subtitle: Text(profile?.displayname ?? client.userID.localpart),
onTap: () => setDisplaynameAction(context),
),
ListTile(
leading: Icon(Icons.phone_outlined),
title: Text(L10n.of(context).editJitsiInstance),
subtitle: Text(Matrix.of(context).jitsiInstance),
onTap: () => setJitsiInstanceAction(context),
),
ListTile(
leading: Icon(Icons.block_outlined),
title: Text(L10n.of(context).ignoredUsers),
onTap: () async => await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
SettingsIgnoreListView(),
),
),
),
Divider(thickness: 1),
ListTile(
leading: Icon(Icons.vpn_key_outlined),
title: Text(L10n.of(context).changeThePassword),
onTap: () => _changePasswordAccountAction(context),
),
ListTile(
leading: Icon(Icons.exit_to_app_outlined),
title: Text(L10n.of(context).logout),
onTap: () => logoutAction(context),
),
ListTile(
leading: Icon(Icons.delete_forever_outlined),
title: Text(
L10n.of(context).deleteAccount,
style: TextStyle(color: Colors.red),
),
onTap: () => _deleteAccountAction(context),
),
],
),
);
}
}

49
lib/views/settings/settings_chat.dart

@ -0,0 +1,49 @@
import 'package:furrychat/views/settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
class ChatSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: Settings(currentSetting: SettingsViews.chat),
secondScaffold: ChatSettings(),
);
}
}
class ChatSettings extends StatefulWidget {
@override
_ChatSettingsState createState() => _ChatSettingsState();
}
class _ChatSettingsState extends State<ChatSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(L10n.of(context).chat)),
body: ListView(
children: [
ListTile(
title: Text(L10n.of(context).renderRichContent),
trailing: Switch(
value: Matrix.of(context).renderHtml,
activeColor: Theme.of(context).primaryColor,
onChanged: (bool newValue) async {
Matrix.of(context).renderHtml = newValue;
await Matrix.of(context)
.store
.setItem('chat.fluffy.renderHtml', newValue ? '1' : '0');
setState(() => null);
},
),
),
],
),
);
}
}

12
lib/views/settings_devices.dart → lib/views/settings/settings_devices.dart

@ -1,19 +1,19 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:furrychat/views/settings.dart';
import 'package:furrychat/components/dialogs/simple_dialogs.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../components/adaptive_page_layout.dart';
import '../components/matrix.dart';
import '../utils/date_time_extension.dart';
import 'chat_list.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/utils/date_time_extension.dart';
class DevicesSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
firstScaffold: Settings(currentSetting: SettingsViews.devices),
secondScaffold: DevicesSettings(),
);
}
@ -110,7 +110,7 @@ class DevicesSettingsState extends State<DevicesSettings> {
L10n.of(context).removeAllOtherDevices,
style: TextStyle(color: Colors.red),
),
trailing: Icon(Icons.delete_outline),
leading: Icon(Icons.delete_outline_outlined),
onTap: () => _removeDevicesAction(context, devices),
),
Divider(height: 1),

10
lib/views/settings_emotes.dart → lib/views/settings/settings_emotes.dart

@ -9,10 +9,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart';
import '../components/adaptive_page_layout.dart';
import '../components/dialogs/simple_dialogs.dart';
import '../components/matrix.dart';
import 'chat_list.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/dialogs/simple_dialogs.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/views/settings.dart';
class EmotesSettingsView extends StatelessWidget {
final Room room;
@ -24,7 +24,7 @@ class EmotesSettingsView extends StatelessWidget {
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
firstScaffold: Settings(currentSetting: SettingsViews.emotes),
secondScaffold: EmotesSettings(room: room, stateKey: stateKey),
);
}

234
lib/views/settings/settings_encryption.dart

@ -0,0 +1,234 @@
import 'package:furrychat/views/settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:furrychat/utils/beautify_string_extension.dart';
import 'package:furrychat/components/dialogs/simple_dialogs.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:olm/olm.dart' as olm;
class EncryptionSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: Settings(currentSetting: SettingsViews.encryption),
secondScaffold: EncryptionSettings(),
);
}
}
class EncryptionSettings extends StatefulWidget {
@override
_EncryptionSettingsState createState() => _EncryptionSettingsState();
}
class _EncryptionSettingsState extends State<EncryptionSettings> {
Future<dynamic> profileFuture;
dynamic profile;
Future<bool> crossSigningCachedFuture;
bool crossSigningCached;
Future<bool> megolmBackupCachedFuture;
bool megolmBackupCached;
Future<void> requestSSSSCache(BuildContext context) async {
final handle = Matrix.of(context).client.encryption.ssss.open();
final str = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).askSSSSCache,
hintText: L10n.of(context).passphraseOrKey,
password: true,
);
if (str != null) {
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
handle.unlock(recoveryKey: str);
valid = true;
} catch (e, s) {
debugPrint('Couldn\'t use recovery key: ' + e.toString());
debugPrint(s.toString());
try {
handle.unlock(passphrase: str);
valid = true;
} catch (e, s) {
debugPrint('Couldn\'t use recovery passphrase: ' + e.toString());
debugPrint(s.toString());
valid = false;
}
}
await Navigator.of(context)?.pop();
if (valid) {
await handle.maybeCacheAll();
await SimpleDialogs(context).inform(
contentText: L10n.of(context).cachedKeys,
);
setState(() {
crossSigningCachedFuture = null;
crossSigningCached = null;
megolmBackupCachedFuture = null;
megolmBackupCached = null;
});
} else {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
}
}
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
profileFuture ??= client.ownProfile.then((p) {
if (mounted) setState(() => profile = p);
return p;
});
crossSigningCachedFuture ??=
client.encryption.crossSigning.isCached().then((c) {
if (mounted) setState(() => crossSigningCached = c);
return c;
});
megolmBackupCachedFuture ??=
client.encryption.keyManager.isCached().then((c) {
if (mounted) setState(() => megolmBackupCached = c);
return c;
});
return Scaffold(
appBar: AppBar(title: Text(L10n.of(context).encryption)),
body: ListView(
children: [
ListTile(
leading: Icon(Icons.compare_arrows_outlined),
title: Text(client.encryption.crossSigning.enabled
? L10n.of(context).crossSigningEnabled
: L10n.of(context).crossSigningDisabled),
subtitle: client.encryption.crossSigning.enabled
? Text(client.isUnknownSession
? L10n.of(context).unknownSessionVerify
: L10n.of(context).sessionVerified +
', ' +
(crossSigningCached == null
? ''
: (crossSigningCached
? L10n.of(context).keysCached
: L10n.of(context).keysMissing)))
: null,
onTap: () async {
if (!client.encryption.crossSigning.enabled) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).noCrossSignBootstrap,
);
return;
}
if (client.isUnknownSession) {
final str = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).askSSSSVerify,
hintText: L10n.of(context).passphraseOrKey,
password: true,
);
if (str != null) {
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
await client.encryption.crossSigning
.selfSign(recoveryKey: str);
valid = true;
} catch (_) {
try {
await client.encryption.crossSigning
.selfSign(passphrase: str);
valid = true;
} catch (_) {
valid = false;
}
}
await Navigator.of(context)?.pop();
if (valid) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).verifiedSession,
);
setState(() {
crossSigningCachedFuture = null;
crossSigningCached = null;
megolmBackupCachedFuture = null;
megolmBackupCached = null;
});
} else {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
}
}
if (!(await client.encryption.crossSigning.isCached())) {
await requestSSSSCache(context);
}
},
),
ListTile(
leading: Icon(Icons.wb_cloudy_outlined),
title: Text(client.encryption.keyManager.enabled
? L10n.of(context).onlineKeyBackupEnabled
: L10n.of(context).onlineKeyBackupDisabled),
subtitle: client.encryption.keyManager.enabled
? Text(megolmBackupCached == null
? ''
: (megolmBackupCached
? L10n.of(context).keysCached
: L10n.of(context).keysMissing))
: null,
onTap: () async {
if (!client.encryption.keyManager.enabled) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).noMegolmBootstrap,
);
return;
}
if (!(await client.encryption.keyManager.isCached())) {
await requestSSSSCache(context);
}
},
),
Divider(thickness: 1),
ListTile(
title: Text('Device name:'),
subtitle: Text(client.userDeviceKeys[client.userID]
?.deviceKeys[client.deviceID]?.deviceDisplayName ??
L10n.of(context).unknownDevice),
),
ListTile(
title: Text('Device ID:'),
subtitle: Text(client.deviceID),
),
ListTile(
title: Text('Encryption enabled:'),
subtitle: Text(client.encryptionEnabled.toString()),
),
if (client.encryptionEnabled)
Column(
children: <Widget>[
ListTile(
title: Text('Your public fingerprint key:'),
subtitle: Text(client.fingerprintKey.beautified),
),
ListTile(
title: Text('Your public identity key:'),
subtitle: Text(client.identityKey.beautified),
),
ListTile(
title: Text('LibOlm version:'),
subtitle: Text(olm.get_library_version().join('.')),
),
],
),
],
),
);
}
}

44
lib/views/settings/settings_homeserver.dart

@ -0,0 +1,44 @@
import 'package:furrychat/views/settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart';
class HomeserverSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: Settings(currentSetting: SettingsViews.homeserver),
secondScaffold: HomeserverSettings(),
);
}
}
class HomeserverSettings extends StatefulWidget {
@override
_HomeserverSettingsState createState() => _HomeserverSettingsState();
}
class _HomeserverSettingsState extends State<HomeserverSettings> {
@override
Widget build(BuildContext context) {
var client = Matrix.of(context).client;
return Scaffold(
appBar: AppBar(title: Text(L10n.of(context).homeserver)),
body: ListView(
children: [
ListTile(
title: Text(L10n.of(context).yourOwnUsername + ':'),
subtitle: Text(client.userID),
),
ListTile(
title: Text('Homeserver:'),
subtitle: Text(client.homeserver.toString()),
),
],
),
);
}
}

6
lib/views/settings_ignore_list.dart → lib/views/settings/settings_ignore_list.dart

@ -5,15 +5,15 @@ import 'package:furrychat/components/dialogs/simple_dialogs.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../components/matrix.dart';
import 'chat_list.dart';
import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/views/settings.dart';
class SettingsIgnoreListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
firstScaffold: Settings(currentSetting: SettingsViews.account),
secondScaffold: SettingsIgnoreList(),
);
}

8
lib/views/settings_multiple_emotes.dart → lib/views/settings/settings_multiple_emotes.dart

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../components/adaptive_page_layout.dart';
import '../utils/app_route.dart';
import 'chat_list.dart';
import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/utils/app_route.dart';
import 'package:furrychat/views/settings.dart';
import 'settings_emotes.dart';
class MultipleEmotesSettingsView extends StatelessWidget {
@ -15,7 +15,7 @@ class MultipleEmotesSettingsView extends StatelessWidget {
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
firstScaffold: Settings(currentSetting: SettingsViews.emotes),
secondScaffold: MultipleEmotesSettings(room: room),
);
}

155
lib/views/settings/settings_themes.dart

@ -0,0 +1,155 @@
import 'dart:io';
import 'package:furrychat/views/settings.dart';
import 'package:flutter/material.dart';