Skip to content

Commit

Permalink
feat: add settings dialog and display edit events properly (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
Feichtmeier authored Jan 9, 2025
1 parent 31415f3 commit 6b33d84
Show file tree
Hide file tree
Showing 25 changed files with 602 additions and 165 deletions.
6 changes: 4 additions & 2 deletions lib/chat/authentication/authentication_model.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:matrix/matrix.dart';
import 'package:safe_change_notifier/safe_change_notifier.dart';

import '../../common/logging.dart';

class AuthenticationModel extends SafeChangeNotifier {
AuthenticationModel({required Client client}) : _client = client;

Expand Down Expand Up @@ -39,11 +41,11 @@ class AuthenticationModel extends SafeChangeNotifier {
);
await _client.firstSyncReceived;
await _client.roomsLoading;

await _loadMediaConfig();
await onSuccess();
} on Exception catch (e) {
} on Exception catch (e, s) {
await onFail(e.toString());
printMessageInDebugMode(e, s);
} finally {
_setProcessingAccess(false);
}
Expand Down
19 changes: 17 additions & 2 deletions lib/chat/bootstrap/bootstrap_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,23 @@ class BootstrapModel extends SafeChangeNotifier {
final Client _client;
final FlutterSecureStorage _secureStorage;

Future<bool> isBootrapNeeded() async =>
_client.isUnknownSession && _client.encryption!.crossSigning.enabled;
Future<bool> isBootrapNeeded() async {
if (!_client.encryptionEnabled) return true;
await _client.accountDataLoading;
await _client.userDeviceKeysLoading;
if (_client.prevBatch == null) {
await _client.onSync.stream.first;
}
final crossSigning =
await _client.encryption?.crossSigning.isCached() ?? false;
final needsBootstrap =
await _client.encryption?.keyManager.isCached() == false ||
_client.encryption?.crossSigning.enabled == false ||
crossSigning == false;
final isUnknownSession = _client.isUnknownSession;

return needsBootstrap || isUnknownSession;
}

String get secureStorageKey => 'ssss_recovery_key_${_client.userID}';

Expand Down
4 changes: 3 additions & 1 deletion lib/chat/event_x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import 'package:matrix/matrix.dart';
extension EventX on Event {
bool get isImage => messageType == MessageTypes.Image;

bool get showAsBadge => {
bool get showAsBadge =>
messageType == MessageTypes.Emote ||
{
EventTypes.RoomAvatar,
EventTypes.RoomAliases,
EventTypes.RoomTopic,
Expand Down
2 changes: 1 addition & 1 deletion lib/chat/room_x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension RoomX on Room {
return lastReceipts.toList();
}

bool get canEdit =>
bool get canEditAtleastSomething =>
ownPowerLevel == 100 ||
canKick ||
canBan ||
Expand Down
44 changes: 44 additions & 0 deletions lib/chat/settings/logout_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';

import '../../common/view/confirm.dart';
import '../../l10n/l10n.dart';
import '../authentication/authentication_model.dart';
import '../authentication/chat_login_page.dart';
import '../chat_model.dart';

class LogoutButton extends StatelessWidget {
const LogoutButton({super.key});

@override
Widget build(BuildContext context) {
final chatModel = di<ChatModel>();
final l10n = context.l10n;
return ElevatedButton(
onPressed: () => showDialog(
context: context,
builder: (context) => ConfirmationDialog(
title: Text(l10n.logout),
content: Text(l10n.areYouSureYouWantToLogout),
onConfirm: () {
chatModel.setSelectedRoom(null);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (_) => const ChatLoginPage(),
),
(route) => false,
);
di<AuthenticationModel>().logout(
onFail: (e) => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
),
);
},
),
),
child: Text(l10n.logout),
);
}
}
123 changes: 123 additions & 0 deletions lib/chat/settings/settings_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';
import 'package:yaru/yaru.dart';

import '../../common/view/snackbars.dart';
import '../../common/view/ui_constants.dart';
import '../../l10n/l10n.dart';
import '../chat_model.dart';
import '../view/chat_master/chat_my_user_avatar.dart';
import 'logout_button.dart';
import 'settings_model.dart';

class SettingsDialog extends StatefulWidget with WatchItStatefulWidgetMixin {
const SettingsDialog({super.key});

@override
State<SettingsDialog> createState() => _SettingsDialogState();
}

class _SettingsDialogState extends State<SettingsDialog> {
late final TextEditingController _displayNameController;
late final TextEditingController _idController;

@override
void initState() {
super.initState();
_displayNameController = TextEditingController();
_idController = TextEditingController(text: di<ChatModel>().myUserId);
di<SettingsModel>().getMyProfile().then((v) {
_displayNameController.text = v?.displayName ?? '';
_idController.text = v?.userId ?? '';
});
}

@override
void dispose() {
super.dispose();
_displayNameController.dispose();
_idController.dispose();
}

@override
Widget build(BuildContext context) {
final l10n = context.l10n;
watchFuture(
(SettingsModel m) => m.getMyProfile(),
initialValue: di<SettingsModel>().myProfile,
preserveState: false,
);
final profile = watchStream(
(SettingsModel m) => m.myProfileStream,
initialValue: di<SettingsModel>().myProfile,
preserveState: false,
).data;

return AlertDialog(
titlePadding: EdgeInsets.zero,
title: YaruDialogTitleBar(
title: Text(l10n.settings),
border: BorderSide.none,
backgroundColor: Colors.transparent,
),
scrollable: true,
content: SizedBox(
height: 800,
width: 500,
child: Column(
spacing: 2 * kBigPadding,
children: [
ChatMyUserAvatar(
key: ValueKey(profile?.avatarUrl),
uri: profile?.avatarUrl,
dimension: 100,
iconSize: 70,
),
YaruSection(
child: Column(
children: [
YaruTile(
title: TextField(
controller: _displayNameController,
decoration: InputDecoration(
suffixIcon: IconButton(
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(6),
bottomRight: Radius.circular(6),
),
),
),
onPressed: profile?.displayName !=
_displayNameController.text
? () => di<SettingsModel>().setDisplayName(
name: _displayNameController.text,
onFail: (e) =>
showErrorSnackBar(context, e),
)
: null,
icon: const Icon(YaruIcons.save),
),
contentPadding: const EdgeInsets.all(10.5),
label: Text(l10n.editDisplayname),
),
),
),
YaruTile(
title: TextField(
enabled: false,
controller: _idController,
),
trailing: const LogoutButton(),
),
],
),
),
],
),
),
);
}
}
118 changes: 118 additions & 0 deletions lib/chat/settings/settings_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart';
import 'package:matrix/matrix.dart';
import 'package:mime/mime.dart';
import 'package:safe_change_notifier/safe_change_notifier.dart';

import '../../common/logging.dart';

class SettingsModel extends SafeChangeNotifier {
SettingsModel({required Client client}) : _client = client;
final Client _client;

Profile? _myProfile;
Profile? get myProfile => _myProfile;
Future<Profile?> getMyProfile({
Function(String error)? onFail,
}) async {
if (_client.userID == null) return null;
try {
_myProfile = await _client.getProfileFromUserId(
_client.userID!,
);
} on Exception catch (e, s) {
onFail?.call(e.toString());
printMessageInDebugMode(e, s);
}

return _myProfile;
}

bool _attachingAvatar = false;
bool get attachingAvatar => _attachingAvatar;
void setAttachingAvatar(bool value) {
if (value == _attachingAvatar) return;
_attachingAvatar = value;

notifyListeners();
}

Future<void> setMyProfilAvatar({
required Function(String error) onFail,
required Function() onWrongFileFormat,
}) async {
setAttachingAvatar(true);

try {
XFile? xFile;
if (Platform.isLinux) {
xFile = await openFile();
} else {
final result = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.any,
);
xFile = result?.files
.map(
(f) => XFile(
f.path!,
mimeType: lookupMimeType(f.path!),
),
)
.toList()
.firstOrNull;
}

if (xFile == null) {
setAttachingAvatar(false);
return;
}

final mime = xFile.mimeType;
final bytes = await xFile.readAsBytes();
MatrixFile? avatarDraftFile;
if (mime?.startsWith('image') == true) {
avatarDraftFile = await MatrixImageFile.shrink(
bytes: bytes,
name: xFile.name,
mimeType: mime,
maxDimension: 1000,
nativeImplementations: _client.nativeImplementations,
);
} else {
onWrongFileFormat();
}

if (avatarDraftFile != null) {
await _client.setAvatar(avatarDraftFile);
}
} on Exception catch (e, s) {
onFail(e.toString());
printMessageInDebugMode(e, s);
}

setAttachingAvatar(false);
}

Future<void> setDisplayName({
required String name,
required Function(String error) onFail,
}) async {
if (_client.userID == null) return;
try {
await _client.setDisplayName(_client.userID!, name);
} on Exception catch (e, s) {
onFail(e.toString());
printMessageInDebugMode(e, s);
}
}

Future<void> removeProfilAvatar() async {
await _client.setAvatar(null);
}

Stream<Profile?> get myProfileStream =>
_client.onUserProfileUpdate.stream.asyncMap((u) async => getMyProfile());
}
Loading

0 comments on commit 6b33d84

Please sign in to comment.