refactor: Rename views to pages
This commit is contained in:
parent
c8827ae1b2
commit
74606b1600
63 changed files with 77 additions and 77 deletions
51
lib/pages/archive.dart
Normal file
51
lib/pages/archive.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/views/archive_view.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class Archive extends StatefulWidget {
|
||||
@override
|
||||
ArchiveController createState() => ArchiveController();
|
||||
}
|
||||
|
||||
class ArchiveController extends State<Archive> {
|
||||
List<Room> archive;
|
||||
|
||||
Future<List<Room>> getArchive(BuildContext context) async {
|
||||
if (archive != null) return archive;
|
||||
return await Matrix.of(context).client.archive;
|
||||
}
|
||||
|
||||
void forgetAction(int i) => setState(() => archive.removeAt(i));
|
||||
|
||||
void forgetAllAction() async {
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
message: L10n.of(context).clearArchive,
|
||||
useRootNavigator: false,
|
||||
) !=
|
||||
OkCancelResult.ok) {
|
||||
return;
|
||||
}
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
while (archive.isNotEmpty) {
|
||||
Logs().v('Forget room ${archive.last.displayname}');
|
||||
await archive.last.forget();
|
||||
archive.removeLast();
|
||||
}
|
||||
},
|
||||
);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ArchiveView(this);
|
||||
}
|
||||
280
lib/pages/bootstrap_dialog.dart
Normal file
280
lib/pages/bootstrap_dialog.dart
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/encryption/utils/bootstrap.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_flat_button.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
|
||||
import 'key_verification_dialog.dart';
|
||||
|
||||
class BootstrapDialog extends StatefulWidget {
|
||||
final bool wipe;
|
||||
final Client client;
|
||||
const BootstrapDialog({
|
||||
Key key,
|
||||
this.wipe = false,
|
||||
@required this.client,
|
||||
}) : super(key: key);
|
||||
|
||||
Future<bool> show(BuildContext context) => PlatformInfos.isCupertinoStyle
|
||||
? showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => this,
|
||||
useRootNavigator: false,
|
||||
barrierDismissible: true,
|
||||
)
|
||||
: showDialog(
|
||||
context: context,
|
||||
builder: (context) => this,
|
||||
useRootNavigator: false,
|
||||
barrierDismissible: true,
|
||||
);
|
||||
|
||||
@override
|
||||
_BootstrapDialogState createState() => _BootstrapDialogState();
|
||||
}
|
||||
|
||||
class _BootstrapDialogState extends State<BootstrapDialog> {
|
||||
final TextEditingController _recoveryKeyTextEditingController =
|
||||
TextEditingController();
|
||||
|
||||
Bootstrap bootstrap;
|
||||
|
||||
String _recoveryKeyInputError;
|
||||
|
||||
bool _recoveryKeyInputLoading = false;
|
||||
|
||||
String titleText;
|
||||
|
||||
bool _recoveryKeyStored = false;
|
||||
|
||||
bool _wipe;
|
||||
|
||||
void _createBootstrap(bool wipe) {
|
||||
setState(() {
|
||||
_wipe = wipe;
|
||||
titleText = null;
|
||||
_recoveryKeyStored = false;
|
||||
bootstrap = widget.client.encryption
|
||||
.bootstrap(onUpdate: () => setState(() => null));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_wipe ??= widget.wipe;
|
||||
final buttons = <AdaptiveFlatButton>[];
|
||||
Widget body = LinearProgressIndicator();
|
||||
titleText = L10n.of(context).loadingPleaseWait;
|
||||
|
||||
if (bootstrap == null) {
|
||||
titleText = L10n.of(context).chatBackup;
|
||||
body = Text(L10n.of(context).chatBackupDescription);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).next,
|
||||
onPressed: () => _createBootstrap(false),
|
||||
));
|
||||
} else if (bootstrap.newSsssKey?.recoveryKey != null &&
|
||||
_recoveryKeyStored == false) {
|
||||
final key = bootstrap.newSsssKey.recoveryKey;
|
||||
titleText = L10n.of(context).securityKey;
|
||||
body = Container(
|
||||
alignment: Alignment.center,
|
||||
width: 200,
|
||||
height: 128,
|
||||
child: Text(
|
||||
key,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
wordSpacing: 38,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).copyToClipboard,
|
||||
onPressed: () => Clipboard.setData(ClipboardData(text: key)),
|
||||
));
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).next,
|
||||
onPressed: () => setState(() => _recoveryKeyStored = true),
|
||||
));
|
||||
} else {
|
||||
switch (bootstrap.state) {
|
||||
case BootstrapState.loading:
|
||||
break;
|
||||
case BootstrapState.askWipeSsss:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.wipeSsss(_wipe),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.askUseExistingSsss:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.useExistingSsss(!_wipe),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.askUnlockSsss:
|
||||
throw Exception('This state is not supposed to be implemented');
|
||||
case BootstrapState.askNewSsss:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.newSsss(),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.openExistingSsss:
|
||||
_recoveryKeyStored = true;
|
||||
titleText =
|
||||
_recoveryKeyInputError ?? L10n.of(context).pleaseEnterSecurityKey;
|
||||
body = PlatformInfos.isCupertinoStyle
|
||||
? CupertinoTextField(
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
autofillHints: _recoveryKeyInputLoading
|
||||
? null
|
||||
: [AutofillHints.password],
|
||||
controller: _recoveryKeyTextEditingController,
|
||||
)
|
||||
: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
autofillHints: _recoveryKeyInputLoading
|
||||
? null
|
||||
: [AutofillHints.password],
|
||||
controller: _recoveryKeyTextEditingController,
|
||||
decoration: InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: false,
|
||||
hintText: L10n.of(context).securityKey,
|
||||
),
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).unlockChatBackup,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_recoveryKeyInputError = null;
|
||||
_recoveryKeyInputLoading = true;
|
||||
});
|
||||
try {
|
||||
await bootstrap.newSsssKey.unlock(
|
||||
keyOrPassphrase: _recoveryKeyTextEditingController.text,
|
||||
);
|
||||
await bootstrap.openExistingSsss();
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to unlock SSSS', e, s);
|
||||
setState(() => _recoveryKeyInputError =
|
||||
L10n.of(context).oopsSomethingWentWrong);
|
||||
} finally {
|
||||
setState(() => _recoveryKeyInputLoading = false);
|
||||
}
|
||||
}));
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).transferFromAnotherDevice,
|
||||
onPressed: () async {
|
||||
final req = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.client.userDeviceKeys[widget.client.userID]
|
||||
.startVerification(),
|
||||
);
|
||||
if (req.error != null) return;
|
||||
await KeyVerificationDialog(request: req.result).show(context);
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
},
|
||||
));
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
textColor: Colors.red,
|
||||
label: L10n.of(context).securityKeyLost,
|
||||
onPressed: () async {
|
||||
if (OkCancelResult.ok ==
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context).securityKeyLost,
|
||||
message: L10n.of(context).wipeChatBackup,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructiveAction: true,
|
||||
)) {
|
||||
_createBootstrap(true);
|
||||
}
|
||||
},
|
||||
));
|
||||
break;
|
||||
case BootstrapState.askWipeCrossSigning:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.wipeCrossSigning(_wipe),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.askSetupCrossSigning:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.askSetupCrossSigning(
|
||||
setupMasterKey: true,
|
||||
setupSelfSigningKey: true,
|
||||
setupUserSigningKey: true,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.askWipeOnlineKeyBackup:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.wipeOnlineKeyBackup(_wipe),
|
||||
);
|
||||
|
||||
break;
|
||||
case BootstrapState.askSetupOnlineKeyBackup:
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => bootstrap.askSetupOnlineKeyBackup(true),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.askBadSsss:
|
||||
case BootstrapState.error:
|
||||
titleText = L10n.of(context).oopsSomethingWentWrong;
|
||||
body = ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.error_outline, color: Colors.red),
|
||||
title: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).close,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||
));
|
||||
break;
|
||||
case BootstrapState.done:
|
||||
titleText = L10n.of(context).everythingReady;
|
||||
body = ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.check_circle, color: Colors.green),
|
||||
title: Text(L10n.of(context).keysCached),
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).close,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final title = Text(titleText);
|
||||
if (PlatformInfos.isCupertinoStyle) {
|
||||
return CupertinoAlertDialog(
|
||||
title: title,
|
||||
content: body,
|
||||
actions: buttons,
|
||||
);
|
||||
}
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
content: body,
|
||||
actions: buttons,
|
||||
);
|
||||
}
|
||||
}
|
||||
657
lib/pages/chat.dart
Normal file
657
lib/pages/chat.dart
Normal file
|
|
@ -0,0 +1,657 @@
|
|||
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:file_picker_cross/file_picker_cross.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/views/chat_ui.dart';
|
||||
import 'package:fluffychat/pages/recording_dialog.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'send_file_dialog.dart';
|
||||
import '../utils/filtered_timeline_extension.dart';
|
||||
import '../utils/matrix_file_extension.dart';
|
||||
|
||||
class Chat extends StatefulWidget {
|
||||
final String id;
|
||||
final String scrollToEventId;
|
||||
|
||||
Chat(this.id, {Key key, this.scrollToEventId})
|
||||
: super(key: key ?? Key('chatroom-$id'));
|
||||
|
||||
@override
|
||||
ChatController createState() => ChatController();
|
||||
}
|
||||
|
||||
class ChatController extends State<Chat> {
|
||||
Room room;
|
||||
|
||||
Timeline timeline;
|
||||
|
||||
MatrixState matrix;
|
||||
|
||||
final AutoScrollController scrollController = AutoScrollController();
|
||||
|
||||
FocusNode inputFocus = FocusNode();
|
||||
|
||||
Timer typingCoolDown;
|
||||
Timer typingTimeout;
|
||||
bool currentlyTyping = false;
|
||||
|
||||
List<Event> selectedEvents = [];
|
||||
|
||||
List<Event> filteredEvents;
|
||||
|
||||
final Set<String> unfolded = {};
|
||||
|
||||
Event replyEvent;
|
||||
|
||||
Event editEvent;
|
||||
|
||||
bool showScrollDownButton = false;
|
||||
|
||||
bool get selectMode => selectedEvents.isNotEmpty;
|
||||
|
||||
final int _loadHistoryCount = 100;
|
||||
|
||||
String inputText = '';
|
||||
|
||||
String pendingText = '';
|
||||
|
||||
bool get canLoadMore => timeline.events.last.type != EventTypes.RoomCreate;
|
||||
|
||||
bool showEmojiPicker = false;
|
||||
|
||||
void startCallAction() async {
|
||||
final url =
|
||||
'${AppConfig.jitsiInstance}${Uri.encodeComponent(Matrix.of(context).client.generateUniqueTransactionId())}';
|
||||
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.sendEvent({
|
||||
'msgtype': Matrix.callNamespace,
|
||||
'body': url,
|
||||
}));
|
||||
if (success.error != null) return;
|
||||
await launch(url);
|
||||
}
|
||||
|
||||
void requestHistory() async {
|
||||
if (canLoadMore) {
|
||||
try {
|
||||
await timeline.requestHistory(historyCount: _loadHistoryCount);
|
||||
} catch (err) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(err.toLocalizedString(context))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateScrollController() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (scrollController.position.pixels ==
|
||||
scrollController.position.maxScrollExtent &&
|
||||
timeline.events.isNotEmpty &&
|
||||
timeline.events[timeline.events.length - 1].type !=
|
||||
EventTypes.RoomCreate) {
|
||||
requestHistory();
|
||||
}
|
||||
if (scrollController.position.pixels > 0 && showScrollDownButton == false) {
|
||||
setState(() => showScrollDownButton = true);
|
||||
} else if (scrollController.position.pixels == 0 &&
|
||||
showScrollDownButton == true) {
|
||||
setState(() => showScrollDownButton = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scrollController.addListener(_updateScrollController);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void updateView() {
|
||||
if (!mounted) return;
|
||||
setState(
|
||||
() {
|
||||
filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void unfold(String eventId) {
|
||||
var i = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||
setState(() {
|
||||
while (i < filteredEvents.length - 1 && filteredEvents[i].isState) {
|
||||
unfolded.add(filteredEvents[i].eventId);
|
||||
i++;
|
||||
}
|
||||
filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> getTimeline() async {
|
||||
if (timeline == null) {
|
||||
timeline = await room.getTimeline(onUpdate: updateView);
|
||||
if (timeline.events.isNotEmpty) {
|
||||
// ignore: unawaited_futures
|
||||
room.setUnread(false).catchError((err) {
|
||||
if (err is MatrixException && err.errcode == 'M_FORBIDDEN') {
|
||||
// ignore if the user is not in the room (still joining)
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// when the scroll controller is attached we want to scroll to an event id, if specified
|
||||
// and update the scroll controller...which will trigger a request history, if the
|
||||
// "load more" button is visible on the screen
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
if (mounted) {
|
||||
if (widget.scrollToEventId != null) {
|
||||
scrollToEventId(widget.scrollToEventId);
|
||||
}
|
||||
_updateScrollController();
|
||||
}
|
||||
});
|
||||
}
|
||||
filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
|
||||
if (room.notificationCount != null &&
|
||||
room.notificationCount > 0 &&
|
||||
timeline != null &&
|
||||
timeline.events.isNotEmpty &&
|
||||
Matrix.of(context).webHasFocus) {
|
||||
// ignore: unawaited_futures
|
||||
room.setReadMarker(
|
||||
timeline.events.first.eventId,
|
||||
readReceiptLocationEventId: timeline.events.first.eventId,
|
||||
);
|
||||
if (PlatformInfos.isIOS) {
|
||||
// Workaround for iOS not clearing notifications with fcm_shared_isolate
|
||||
if (!room.client.rooms.any((r) =>
|
||||
r.membership == Membership.invite ||
|
||||
(r.notificationCount != null && r.notificationCount > 0))) {
|
||||
// ignore: unawaited_futures
|
||||
FlutterLocalNotificationsPlugin().cancelAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timeline?.cancelSubscriptions();
|
||||
timeline = null;
|
||||
matrix.client.activeRoomId = '';
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
TextEditingController sendController = TextEditingController();
|
||||
|
||||
void send() {
|
||||
if (sendController.text.trim().isEmpty) return;
|
||||
room.sendTextEvent(sendController.text,
|
||||
inReplyTo: replyEvent, editEventId: editEvent?.eventId);
|
||||
sendController.value = TextEditingValue(
|
||||
text: pendingText,
|
||||
selection: TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
inputText = pendingText;
|
||||
replyEvent = null;
|
||||
editEvent = null;
|
||||
pendingText = '';
|
||||
});
|
||||
}
|
||||
|
||||
void sendFileAction() async {
|
||||
final result =
|
||||
await FilePickerCross.importFromStorage(type: FileTypeCross.any);
|
||||
if (result == null) return;
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
file: MatrixFile(
|
||||
bytes: result.toUint8List(),
|
||||
name: result.fileName,
|
||||
).detectFileType,
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void sendImageAction() async {
|
||||
final result =
|
||||
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||
if (result == null) return;
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
file: MatrixImageFile(
|
||||
bytes: result.toUint8List(),
|
||||
name: result.fileName,
|
||||
),
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void openCameraAction() async {
|
||||
final file = await ImagePicker().getImage(source: ImageSource.camera);
|
||||
if (file == null) return;
|
||||
final bytes = await file.readAsBytes();
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
file: MatrixImageFile(
|
||||
bytes: bytes,
|
||||
name: file.path,
|
||||
),
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void voiceMessageAction() async {
|
||||
if (await Permission.microphone.isGranted != true) {
|
||||
final status = await Permission.microphone.request();
|
||||
if (status != PermissionStatus.granted) return;
|
||||
}
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (c) => RecordingDialog(),
|
||||
useRootNavigator: false,
|
||||
);
|
||||
if (result == null) return;
|
||||
final audioFile = File(result);
|
||||
// as we already explicitly say send in the recording dialog,
|
||||
// we do not need the send file dialog anymore. We can just send this straight away.
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.sendFileEvent(
|
||||
MatrixAudioFile(
|
||||
bytes: audioFile.readAsBytesSync(), name: audioFile.path),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getSelectedEventString() {
|
||||
var copyString = '';
|
||||
if (selectedEvents.length == 1) {
|
||||
return selectedEvents.first
|
||||
.getDisplayEvent(timeline)
|
||||
.getLocalizedBody(MatrixLocals(L10n.of(context)));
|
||||
}
|
||||
for (final event in selectedEvents) {
|
||||
if (copyString.isNotEmpty) copyString += '\n\n';
|
||||
copyString += event.getDisplayEvent(timeline).getLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
withSenderNamePrefix: true);
|
||||
}
|
||||
return copyString;
|
||||
}
|
||||
|
||||
void copyEventsAction() {
|
||||
Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
|
||||
setState(() => selectedEvents.clear());
|
||||
}
|
||||
|
||||
void reportEventAction() async {
|
||||
final event = selectedEvents.single;
|
||||
final score = await showConfirmationDialog<int>(
|
||||
context: context,
|
||||
title: L10n.of(context).howOffensiveIsThisContent,
|
||||
useRootNavigator: false,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context).extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context).offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context).inoffensive,
|
||||
),
|
||||
]);
|
||||
if (score == null) return;
|
||||
final reason = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context).reason)]);
|
||||
if (reason == null || reason.single.isEmpty) return;
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.reportContent(
|
||||
event.roomId,
|
||||
event.eventId,
|
||||
reason.single,
|
||||
score,
|
||||
),
|
||||
);
|
||||
if (result.error != null) return;
|
||||
setState(() => selectedEvents.clear());
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).contentHasBeenReported)));
|
||||
}
|
||||
|
||||
void redactEventsAction() async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).messageWillBeRemovedWarning,
|
||||
okLabel: L10n.of(context).remove,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.ok;
|
||||
if (!confirmed) return;
|
||||
for (final event in selectedEvents) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
event.status > 0 ? event.redactEvent() : event.remove());
|
||||
}
|
||||
setState(() => selectedEvents.clear());
|
||||
}
|
||||
|
||||
bool get canRedactSelectedEvents {
|
||||
for (final event in selectedEvents) {
|
||||
if (event.canRedact == false) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void forwardEventsAction() async {
|
||||
if (selectedEvents.length == 1) {
|
||||
Matrix.of(context).shareContent = selectedEvents.first.content;
|
||||
} else {
|
||||
Matrix.of(context).shareContent = {
|
||||
'msgtype': 'm.text',
|
||||
'body': _getSelectedEventString(),
|
||||
};
|
||||
}
|
||||
setState(() => selectedEvents.clear());
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
}
|
||||
|
||||
void sendAgainAction() {
|
||||
final event = selectedEvents.first;
|
||||
if (event.status == -1) {
|
||||
event.sendAgain();
|
||||
}
|
||||
final allEditEvents = event
|
||||
.aggregatedEvents(timeline, RelationshipTypes.edit)
|
||||
.where((e) => e.status == -1);
|
||||
for (final e in allEditEvents) {
|
||||
e.sendAgain();
|
||||
}
|
||||
setState(() => selectedEvents.clear());
|
||||
}
|
||||
|
||||
void replyAction({Event replyTo}) {
|
||||
setState(() {
|
||||
replyEvent = replyTo ?? selectedEvents.first;
|
||||
selectedEvents.clear();
|
||||
});
|
||||
inputFocus.requestFocus();
|
||||
}
|
||||
|
||||
void scrollToEventId(String eventId) async {
|
||||
var eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||
if (eventIndex == -1) {
|
||||
// event id not found...maybe we can fetch it?
|
||||
// the try...finally is here to start and close the loading dialog reliably
|
||||
final task = Future.microtask(() async {
|
||||
// okay, we first have to fetch if the event is in the room
|
||||
try {
|
||||
final event = await timeline.getEventById(eventId);
|
||||
if (event == null) {
|
||||
// event is null...meaning something is off
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||
// event wasn't found, as the server gave a 404 or something
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
// okay, we know that the event *is* in the room
|
||||
while (eventIndex == -1) {
|
||||
if (!canLoadMore) {
|
||||
// we can't load any more events but still haven't found ours yet...better stop here
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await timeline.requestHistory(historyCount: _loadHistoryCount);
|
||||
} catch (err) {
|
||||
if (err is TimeoutException) {
|
||||
// loading the history timed out...so let's do nothing
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||
}
|
||||
});
|
||||
if (context != null) {
|
||||
await showFutureLoadingDialog(context: context, future: () => task);
|
||||
} else {
|
||||
await task;
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await scrollController.scrollToIndex(eventIndex,
|
||||
preferPosition: AutoScrollPosition.middle);
|
||||
_updateScrollController();
|
||||
}
|
||||
|
||||
void scrollDown() => scrollController.jumpTo(0);
|
||||
|
||||
void onEmojiSelected(category, emoji) {
|
||||
setState(() => showEmojiPicker = false);
|
||||
if (emoji == null) return;
|
||||
// make sure we don't send the same emoji twice
|
||||
if (_allReactionEvents
|
||||
.any((e) => e.content['m.relates_to']['key'] == emoji.emoji)) return;
|
||||
return sendEmojiAction(emoji.emoji);
|
||||
}
|
||||
|
||||
Iterable<Event> _allReactionEvents;
|
||||
|
||||
void cancelEmojiPicker() => setState(() => showEmojiPicker = false);
|
||||
|
||||
void pickEmojiAction(Iterable<Event> allReactionEvents) async {
|
||||
_allReactionEvents = allReactionEvents;
|
||||
setState(() => showEmojiPicker = true);
|
||||
}
|
||||
|
||||
void sendEmojiAction(String emoji) async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.sendReaction(
|
||||
selectedEvents.single.eventId,
|
||||
emoji,
|
||||
),
|
||||
);
|
||||
setState(() => selectedEvents.clear());
|
||||
}
|
||||
|
||||
void clearSelectedEvents() => setState(() {
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
});
|
||||
|
||||
void editSelectedEventAction() {
|
||||
setState(() {
|
||||
pendingText = sendController.text;
|
||||
editEvent = selectedEvents.first;
|
||||
inputText = sendController.text = editEvent
|
||||
.getDisplayEvent(timeline)
|
||||
.getLocalizedBody(MatrixLocals(L10n.of(context)),
|
||||
withSenderNamePrefix: false, hideReply: true);
|
||||
selectedEvents.clear();
|
||||
});
|
||||
inputFocus.requestFocus();
|
||||
}
|
||||
|
||||
void onEventActionPopupMenuSelected(selected) {
|
||||
switch (selected) {
|
||||
case 'copy':
|
||||
copyEventsAction();
|
||||
break;
|
||||
case 'redact':
|
||||
redactEventsAction();
|
||||
break;
|
||||
case 'report':
|
||||
reportEventAction();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void goToNewRoomAction() async {
|
||||
if (OkCancelResult.ok !=
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).goToTheNewRoom,
|
||||
message: room
|
||||
.getState(EventTypes.RoomTombstone)
|
||||
.parsedTombstoneContent
|
||||
.body,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client.joinRoom(room
|
||||
.getState(EventTypes.RoomTombstone)
|
||||
.parsedTombstoneContent
|
||||
.replacementRoom),
|
||||
);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.leave,
|
||||
);
|
||||
if (result.error == null) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamedAndRemoveUntilIsFirst('/rooms/${result.result}');
|
||||
}
|
||||
}
|
||||
|
||||
void onSelectMessage(Event event) {
|
||||
if (!event.redacted) {
|
||||
if (selectedEvents.contains(event)) {
|
||||
setState(
|
||||
() => selectedEvents.remove(event),
|
||||
);
|
||||
} else {
|
||||
setState(
|
||||
() => selectedEvents.add(event),
|
||||
);
|
||||
}
|
||||
selectedEvents.sort(
|
||||
(a, b) => a.originServerTs.compareTo(b.originServerTs),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int findChildIndexCallback(Key key, Map<String, int> thisEventsKeyMap) {
|
||||
// this method is called very often. As such, it has to be optimized for speed.
|
||||
if (!(key is ValueKey)) {
|
||||
return null;
|
||||
}
|
||||
final eventId = (key as ValueKey).value;
|
||||
if (!(eventId is String)) {
|
||||
return null;
|
||||
}
|
||||
// first fetch the last index the event was at
|
||||
final index = thisEventsKeyMap[eventId];
|
||||
if (index == null) {
|
||||
return null;
|
||||
}
|
||||
// we need to +1 as 0 is the typing thing at the bottom
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
void onInputBarSubmitted(String text) {
|
||||
send();
|
||||
FocusScope.of(context).requestFocus(inputFocus);
|
||||
}
|
||||
|
||||
void onAddPopupMenuButtonSelected(String choice) {
|
||||
if (choice == 'file') {
|
||||
sendFileAction();
|
||||
} else if (choice == 'image') {
|
||||
sendImageAction();
|
||||
}
|
||||
if (choice == 'camera') {
|
||||
openCameraAction();
|
||||
}
|
||||
if (choice == 'voice') {
|
||||
voiceMessageAction();
|
||||
}
|
||||
}
|
||||
|
||||
void onInputBarChanged(String text) {
|
||||
typingCoolDown?.cancel();
|
||||
typingCoolDown = Timer(Duration(seconds: 2), () {
|
||||
typingCoolDown = null;
|
||||
currentlyTyping = false;
|
||||
room.setTyping(false);
|
||||
});
|
||||
typingTimeout ??= Timer(Duration(seconds: 30), () {
|
||||
typingTimeout = null;
|
||||
currentlyTyping = false;
|
||||
});
|
||||
if (!currentlyTyping) {
|
||||
currentlyTyping = true;
|
||||
room.setTyping(true, timeout: Duration(seconds: 30).inMilliseconds);
|
||||
}
|
||||
setState(() => inputText = text);
|
||||
}
|
||||
|
||||
void cancelReplyEventAction() => setState(() {
|
||||
if (editEvent != null) {
|
||||
inputText = sendController.text = pendingText;
|
||||
pendingText = '';
|
||||
}
|
||||
replyEvent = null;
|
||||
editEvent = null;
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatUI(this);
|
||||
}
|
||||
303
lib/pages/chat_details.dart
Normal file
303
lib/pages/chat_details.dart
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
||||
import 'package:fluffychat/pages/views/chat_details_ui.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
enum AliasActions { copy, delete, setCanonical }
|
||||
|
||||
class ChatDetails extends StatefulWidget {
|
||||
final String roomId;
|
||||
|
||||
const ChatDetails(this.roomId);
|
||||
|
||||
@override
|
||||
ChatDetailsController createState() => ChatDetailsController();
|
||||
}
|
||||
|
||||
class ChatDetailsController extends State<ChatDetails> {
|
||||
List<User> members;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
members ??=
|
||||
Matrix.of(context).client.getRoomById(widget.roomId).getParticipants();
|
||||
}
|
||||
|
||||
void setDisplaynameAction() async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).changeTheNameOfTheGroup,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
initialText: room.getLocalizedDisplayname(
|
||||
MatrixLocals(
|
||||
L10n.of(context),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setName(input.single),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged)));
|
||||
}
|
||||
}
|
||||
|
||||
void editAliases() async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
|
||||
// The current endpoint doesnt seem to be implemented in Synapse. This may
|
||||
// change in the future and then we just need to switch to this api call:
|
||||
//
|
||||
// final aliases = await showFutureLoadingDialog(
|
||||
// context: context,
|
||||
// future: () => room.client.requestRoomAliases(room.id),
|
||||
// );
|
||||
//
|
||||
// While this is not working we use the unstable api:
|
||||
final aliases = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client
|
||||
.request(
|
||||
RequestType.GET,
|
||||
'/client/unstable/org.matrix.msc2432/rooms/${Uri.encodeComponent(room.id)}/aliases',
|
||||
)
|
||||
.then((response) => List<String>.from(response['aliases'])),
|
||||
);
|
||||
// Switch to the stable api once it is implemented.
|
||||
|
||||
if (aliases.error != null) return;
|
||||
final adminMode = room.canSendEvent('m.room.canonical_alias');
|
||||
if (aliases.result.isEmpty && (room.canonicalAlias?.isNotEmpty ?? false)) {
|
||||
aliases.result.add(room.canonicalAlias);
|
||||
}
|
||||
if (aliases.result.isEmpty && adminMode) {
|
||||
return setAliasAction();
|
||||
}
|
||||
final select = await showConfirmationDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).editRoomAliases,
|
||||
actions: [
|
||||
if (adminMode)
|
||||
AlertDialogAction(label: L10n.of(context).create, key: 'new'),
|
||||
...aliases.result
|
||||
.map((alias) => AlertDialogAction(key: alias, label: alias))
|
||||
.toList(),
|
||||
],
|
||||
);
|
||||
if (select == null) return;
|
||||
if (select == 'new') {
|
||||
return setAliasAction();
|
||||
}
|
||||
final option = await showConfirmationDialog<AliasActions>(
|
||||
context: context,
|
||||
title: select,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
label: L10n.of(context).copyToClipboard,
|
||||
key: AliasActions.copy,
|
||||
isDefaultAction: true,
|
||||
),
|
||||
if (adminMode) ...{
|
||||
AlertDialogAction(
|
||||
label: L10n.of(context).setAsCanonicalAlias,
|
||||
key: AliasActions.setCanonical,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
AlertDialogAction(
|
||||
label: L10n.of(context).delete,
|
||||
key: AliasActions.delete,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
switch (option) {
|
||||
case AliasActions.copy:
|
||||
await Clipboard.setData(ClipboardData(text: select));
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).copiedToClipboard)),
|
||||
);
|
||||
break;
|
||||
case AliasActions.delete:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client.deleteRoomAlias(select),
|
||||
);
|
||||
break;
|
||||
case AliasActions.setCanonical:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client
|
||||
.setRoomStateWithKey(room.id, EventTypes.RoomCanonicalAlias, {
|
||||
'alias': select,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void setAliasAction() async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final domain = room.client.userID.domain;
|
||||
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).setInvitationLink,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
prefixText: '#',
|
||||
suffixText: domain,
|
||||
hintText: L10n.of(context).alias,
|
||||
initialText: room.canonicalAlias?.localpart,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
room.client.setRoomAlias('#' + input.single + ':' + domain, room.id),
|
||||
);
|
||||
}
|
||||
|
||||
void setTopicAction() async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).setGroupDescription,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).setGroupDescription,
|
||||
initialText: room.topic,
|
||||
minLines: 1,
|
||||
maxLines: 4,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setDescription(input.single),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(SnackBar(
|
||||
content: Text(L10n.of(context).groupDescriptionHasBeenChanged)));
|
||||
}
|
||||
}
|
||||
|
||||
void setGuestAccessAction(GuestAccess guestAccess) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.roomId)
|
||||
.setGuestAccess(guestAccess),
|
||||
);
|
||||
|
||||
void setHistoryVisibilityAction(HistoryVisibility historyVisibility) =>
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.roomId)
|
||||
.setHistoryVisibility(historyVisibility),
|
||||
);
|
||||
|
||||
void setJoinRulesAction(JoinRules joinRule) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.roomId)
|
||||
.setJoinRules(joinRule),
|
||||
);
|
||||
|
||||
void goToEmoteSettings() async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
// okay, we need to test if there are any emote state events other than the default one
|
||||
// if so, we need to be directed to a selection screen for which pack we want to look at
|
||||
// otherwise, we just open the normal one.
|
||||
if ((room.states['im.ponies.room_emotes'] ?? <String, Event>{})
|
||||
.keys
|
||||
.any((String s) => s.isNotEmpty)) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamed('/rooms/${room.id}/emotes');
|
||||
} else {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamed('/settings/emotes', arguments: {'room': room});
|
||||
}
|
||||
}
|
||||
|
||||
void setAvatarAction() 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 room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setAvatar(file),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).avatarHasBeenChanged)));
|
||||
}
|
||||
}
|
||||
|
||||
void requestMoreMembersAction() async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final participants = await showFutureLoadingDialog(
|
||||
context: context, future: () => room.requestParticipants());
|
||||
if (participants.error == null) {
|
||||
setState(() => members = participants.result);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatDetailsUI(this);
|
||||
}
|
||||
65
lib/pages/chat_encryption_settings.dart
Normal file
65
lib/pages/chat_encryption_settings.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/views/chat_encryption_settings_ui.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'key_verification_dialog.dart';
|
||||
|
||||
class ChatEncryptionSettings extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const ChatEncryptionSettings(this.id, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChatEncryptionSettingsController createState() =>
|
||||
ChatEncryptionSettingsController();
|
||||
}
|
||||
|
||||
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
|
||||
Future<void> onSelected(
|
||||
BuildContext context, String action, DeviceKeys key) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.id);
|
||||
final unblock = () async {
|
||||
if (key.blocked) {
|
||||
await key.setBlocked(false);
|
||||
}
|
||||
};
|
||||
switch (action) {
|
||||
case 'verify':
|
||||
await unblock();
|
||||
final req = key.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() => null);
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'verify_user':
|
||||
await unblock();
|
||||
final req =
|
||||
await room.client.userDeviceKeys[key.userId].startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() => null);
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'block':
|
||||
if (key.directVerified) {
|
||||
await key.setVerified(false);
|
||||
}
|
||||
await key.setBlocked(true);
|
||||
setState(() => null);
|
||||
break;
|
||||
case 'unblock':
|
||||
await unblock();
|
||||
setState(() => null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatEncryptionSettingsUI(this);
|
||||
}
|
||||
258
lib/pages/chat_list.dart
Normal file
258
lib/pages/chat_list.dart
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
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/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/pages/views/chat_list_ui.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fluffychat/config/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 'package:uni_links/uni_links.dart';
|
||||
import '../main.dart';
|
||||
import '../widgets/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 }
|
||||
enum PopupMenuAction { settings, invite, newGroup, setStatus, archive }
|
||||
|
||||
class ChatList extends StatefulWidget {
|
||||
final String activeChat;
|
||||
|
||||
const ChatList({this.activeChat, Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChatListController createState() => ChatListController();
|
||||
}
|
||||
|
||||
class ChatListController extends State<ChatList> {
|
||||
StreamSubscription _intentDataStreamSubscription;
|
||||
|
||||
StreamSubscription _intentFileStreamSubscription;
|
||||
|
||||
StreamSubscription _intentUriStreamSubscription;
|
||||
|
||||
final selectedRoomIds = <String>{};
|
||||
|
||||
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))) {
|
||||
return;
|
||||
}
|
||||
Matrix.of(context).shareContent = {
|
||||
'msgtype': 'm.text',
|
||||
'body': text,
|
||||
};
|
||||
}
|
||||
|
||||
void _processIncomingUris(String text) async {
|
||||
if (text == null || !text.startsWith(AppConfig.appOpenUrlScheme)) return;
|
||||
if (text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
|
||||
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
|
||||
!RegExp(r'\s').hasMatch(text))) {
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
UrlLauncher(context, text).openMatrixToUrl();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// For receiving shared Uris
|
||||
_intentDataStreamSubscription = linkStream.listen(_processIncomingUris);
|
||||
if (FluffyChatApp.gotInitialLink == false) {
|
||||
FluffyChatApp.gotInitialLink = true;
|
||||
getInitialLink().then(_processIncomingUris);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_initReceiveSharingIntent();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_intentDataStreamSubscription?.cancel();
|
||||
_intentFileStreamSubscription?.cancel();
|
||||
_intentUriStreamSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void toggleSelection(String roomId) {
|
||||
setState(() => selectedRoomIds.contains(roomId)
|
||||
? selectedRoomIds.remove(roomId)
|
||||
: selectedRoomIds.add(roomId));
|
||||
}
|
||||
|
||||
Future<void> toggleUnread() {
|
||||
final room = Matrix.of(context).client.getRoomById(selectedRoomIds.single);
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setUnread(!room.isUnread),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleFavouriteRoom() {
|
||||
final room = Matrix.of(context).client.getRoomById(selectedRoomIds.single);
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setFavourite(!room.isFavourite),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleMuted() {
|
||||
final room = Matrix.of(context).client.getRoomById(selectedRoomIds.single);
|
||||
return showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? PushRuleState.mentionsOnly
|
||||
: PushRuleState.notify),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> archiveAction() async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.ok;
|
||||
if (!confirmed) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => _archiveSelectedRooms(),
|
||||
);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
void setStatus() async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).setStatus,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).statusExampleMessage,
|
||||
),
|
||||
]);
|
||||
if (input == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.setPresence(
|
||||
Matrix.of(context).client.userID,
|
||||
PresenceType.online,
|
||||
statusMsg: input.single,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onPopupMenuSelect(action) {
|
||||
switch (action) {
|
||||
case PopupMenuAction.setStatus:
|
||||
setStatus();
|
||||
break;
|
||||
case PopupMenuAction.settings:
|
||||
AdaptivePageLayout.of(context).pushNamed('/settings');
|
||||
break;
|
||||
case PopupMenuAction.invite:
|
||||
FluffyShare.share(
|
||||
L10n.of(context).inviteText(Matrix.of(context).client.userID,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}'),
|
||||
context);
|
||||
break;
|
||||
case PopupMenuAction.newGroup:
|
||||
AdaptivePageLayout.of(context).pushNamed('/newgroup');
|
||||
break;
|
||||
case PopupMenuAction.archive:
|
||||
AdaptivePageLayout.of(context).pushNamed('/archive');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _archiveSelectedRooms() async {
|
||||
final client = Matrix.of(context).client;
|
||||
while (selectedRoomIds.isNotEmpty) {
|
||||
final roomId = selectedRoomIds.first;
|
||||
await client.getRoomById(roomId).leave();
|
||||
toggleSelection(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> waitForFirstSync() async {
|
||||
final client = Matrix.of(context).client;
|
||||
if (client.prevBatch?.isEmpty ?? true) {
|
||||
await client.onFirstSync.stream.first;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
final selectMode = Matrix.of(context).shareContent != null
|
||||
? SelectMode.share
|
||||
: selectedRoomIds.isEmpty
|
||||
? SelectMode.normal
|
||||
: SelectMode.select;
|
||||
if (selectMode == SelectMode.share) {
|
||||
setState(() => Matrix.of(context).shareContent = null);
|
||||
} else {
|
||||
setState(() => selectedRoomIds.clear());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatListUI(this);
|
||||
}
|
||||
|
||||
enum ChatListPopupMenuItemActions {
|
||||
createGroup,
|
||||
discover,
|
||||
setStatus,
|
||||
inviteContact,
|
||||
settings,
|
||||
}
|
||||
96
lib/pages/chat_permissions_settings.dart
Normal file
96
lib/pages/chat_permissions_settings.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/views/chat_permissions_settings_ui.dart';
|
||||
import 'package:fluffychat/pages/permission_slider_dialog.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class ChatPermissionsSettings extends StatefulWidget {
|
||||
final String roomId;
|
||||
|
||||
const ChatPermissionsSettings(this.roomId, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChatPermissionsSettingsController createState() =>
|
||||
ChatPermissionsSettingsController();
|
||||
}
|
||||
|
||||
class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
||||
void editPowerLevel(BuildContext context, String key, int currentLevel,
|
||||
{String category}) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
if (!room.canSendEvent(EventTypes.RoomPowerLevels)) {
|
||||
AdaptivePageLayout.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(L10n.of(context).noPermission)));
|
||||
return;
|
||||
}
|
||||
final newLevel =
|
||||
await PermissionSliderDialog(initialPermission: currentLevel)
|
||||
.show(context);
|
||||
if (newLevel == null) return;
|
||||
final content = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels).content);
|
||||
if (category != null) {
|
||||
if (!content.containsKey(category)) {
|
||||
content[category] = <String, dynamic>{};
|
||||
}
|
||||
content[category][key] = newLevel;
|
||||
} else {
|
||||
content[key] = newLevel;
|
||||
}
|
||||
inspect(content);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client
|
||||
.setRoomStateWithKey(room.id, EventTypes.RoomPowerLevels, content),
|
||||
);
|
||||
}
|
||||
|
||||
Stream get onChanged => Matrix.of(context).client.onSync.stream.where(
|
||||
(e) =>
|
||||
(e?.rooms?.join?.containsKey(widget.roomId) ?? false) &&
|
||||
(e.rooms.join[widget.roomId]?.timeline?.events
|
||||
?.any((s) => s.type == EventTypes.RoomPowerLevels) ??
|
||||
false),
|
||||
);
|
||||
|
||||
void updateRoomAction(ServerCapabilities capabilities) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final String roomVersion =
|
||||
room.getState(EventTypes.RoomCreate).content['room_version'] ?? '1';
|
||||
final newVersion = await showConfirmationDialog<String>(
|
||||
context: context,
|
||||
title: L10n.of(context).replaceRoomWithNewerVersion,
|
||||
actions: capabilities.mRoomVersions.available.entries
|
||||
.where((r) => r.key != roomVersion)
|
||||
.map((version) => AlertDialogAction(
|
||||
key: version.key,
|
||||
label:
|
||||
'${version.key} (${version.value.toString().split('.').last})'))
|
||||
.toList(),
|
||||
);
|
||||
if (newVersion == null ||
|
||||
OkCancelResult.cancel ==
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
title: L10n.of(context).areYouSure,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.client.upgradeRoom(widget.roomId, newVersion),
|
||||
).then((_) => AdaptivePageLayout.of(context).pop());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatPermissionsSettingsUI(this);
|
||||
}
|
||||
140
lib/pages/device_settings.dart
Normal file
140
lib/pages/device_settings.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:famedlysdk/encryption/utils/key_verification.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/views/device_settings_ui.dart';
|
||||
import 'package:fluffychat/pages/key_verification_dialog.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class DevicesSettings extends StatefulWidget {
|
||||
@override
|
||||
DevicesSettingsController createState() => DevicesSettingsController();
|
||||
}
|
||||
|
||||
class DevicesSettingsController extends State<DevicesSettings> {
|
||||
List<Device> devices;
|
||||
Future<bool> loadUserDevices(BuildContext context) async {
|
||||
if (devices != null) return true;
|
||||
devices = await Matrix.of(context).client.getDevices();
|
||||
return true;
|
||||
}
|
||||
|
||||
void reload() => setState(() => devices = null);
|
||||
|
||||
bool loadingDeletingDevices = false;
|
||||
String errorDeletingDevices;
|
||||
|
||||
void removeDevicesAction(List<Device> devices) async {
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.cancel) return;
|
||||
final matrix = Matrix.of(context);
|
||||
final deviceIds = <String>[];
|
||||
for (final userDevice in devices) {
|
||||
deviceIds.add(userDevice.deviceId);
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
loadingDeletingDevices = true;
|
||||
errorDeletingDevices = null;
|
||||
});
|
||||
await matrix.client.uiaRequestBackground(
|
||||
(auth) => matrix.client.deleteDevices(
|
||||
deviceIds,
|
||||
auth: auth,
|
||||
),
|
||||
);
|
||||
reload();
|
||||
} catch (e, s) {
|
||||
Logs().v('Error while deleting devices', e, s);
|
||||
setState(() => errorDeletingDevices = e.toString());
|
||||
} finally {
|
||||
setState(() => loadingDeletingDevices = false);
|
||||
}
|
||||
}
|
||||
|
||||
void renameDeviceAction(Device device) async {
|
||||
final displayName = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).changeDeviceName,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: device.displayName,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (displayName == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.updateDevice(device.deviceId, displayName: displayName.single),
|
||||
);
|
||||
if (success.error == null) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
void verifyDeviceAction(Device device) async {
|
||||
final req = Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID]
|
||||
.deviceKeys[device.deviceId]
|
||||
.startVerification();
|
||||
req.onUpdate = () {
|
||||
if ({KeyVerificationState.error, KeyVerificationState.done}
|
||||
.contains(req.state)) {
|
||||
setState(() => null);
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
}
|
||||
|
||||
void blockDeviceAction(Device device) async {
|
||||
final key = Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID]
|
||||
.deviceKeys[device.deviceId];
|
||||
if (key.directVerified) {
|
||||
await key.setVerified(false);
|
||||
}
|
||||
await key.setBlocked(true);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
void unblockDeviceAction(Device device) async {
|
||||
final key = Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID]
|
||||
.deviceKeys[device.deviceId];
|
||||
await key.setBlocked(false);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
bool _isOwnDevice(Device userDevice) =>
|
||||
userDevice.deviceId == Matrix.of(context).client.deviceID;
|
||||
|
||||
Device get thisDevice => devices.firstWhere(
|
||||
_isOwnDevice,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
List<Device> get notThisDevice => List<Device>.from(devices)
|
||||
..removeWhere(_isOwnDevice)
|
||||
..sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DevicesSettingsUI(this);
|
||||
}
|
||||
138
lib/pages/homeserver_picker.dart
Normal file
138
lib/pages/homeserver_picker.dart
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/views/homeserver_picker_ui.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../main.dart';
|
||||
import '../utils/localized_exception_extension.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
class HomeserverPicker extends StatefulWidget {
|
||||
@override
|
||||
HomeserverPickerController createState() => HomeserverPickerController();
|
||||
}
|
||||
|
||||
class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
bool isLoading = false;
|
||||
String domain = AppConfig.defaultHomeserver;
|
||||
final TextEditingController homeserverController =
|
||||
TextEditingController(text: AppConfig.defaultHomeserver);
|
||||
StreamSubscription _intentDataStreamSubscription;
|
||||
|
||||
void _loginWithToken(String token) {
|
||||
if (token?.isEmpty ?? true) return;
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.login(
|
||||
type: AuthenticationTypes.token,
|
||||
token: token,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _processIncomingUris(String text) async {
|
||||
if (text == null || !text.startsWith(AppConfig.appOpenUrlScheme)) return;
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
final token = Uri.parse(text).queryParameters['loginToken'];
|
||||
if (token != null) _loginWithToken(token);
|
||||
}
|
||||
|
||||
void _initReceiveUri() {
|
||||
if (!PlatformInfos.isMobile) return;
|
||||
// For receiving shared Uris
|
||||
_intentDataStreamSubscription = linkStream.listen(_processIncomingUris);
|
||||
if (FluffyChatApp.gotInitialLink == false) {
|
||||
FluffyChatApp.gotInitialLink = true;
|
||||
getInitialLink().then(_processIncomingUris);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initReceiveUri();
|
||||
if (kIsWeb) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final token =
|
||||
Uri.parse(html.window.location.href).queryParameters['loginToken'];
|
||||
_loginWithToken(token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_intentDataStreamSubscription?.cancel();
|
||||
}
|
||||
|
||||
/// Starts an analysis of the given homeserver. It uses the current domain and
|
||||
/// makes sure that it is prefixed with https. Then it searches for the
|
||||
/// well-known information and forwards to the login page depending on the
|
||||
/// login type. For SSO login only the app opens the page and otherwise it
|
||||
/// forwards to the route `/signup`.
|
||||
void checkHomeserverAction() async {
|
||||
try {
|
||||
if (domain.isEmpty) throw L10n.of(context).changeTheHomeserver;
|
||||
var homeserver = domain;
|
||||
|
||||
if (!homeserver.startsWith('https://')) {
|
||||
homeserver = 'https://$homeserver';
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
final wellKnown =
|
||||
await Matrix.of(context).client.checkHomeserver(homeserver);
|
||||
|
||||
var jitsi = wellKnown?.content
|
||||
?.tryGet<Map<String, dynamic>>('im.vector.riot.jitsi')
|
||||
?.tryGet<String>('preferredDomain');
|
||||
if (jitsi != null) {
|
||||
if (!jitsi.endsWith('/')) {
|
||||
jitsi += '/';
|
||||
}
|
||||
Logs().v('Found custom jitsi instance $jitsi');
|
||||
await Matrix.of(context)
|
||||
.store
|
||||
.setItem(SettingKeys.jitsiInstance, jitsi);
|
||||
AppConfig.jitsiInstance = jitsi;
|
||||
}
|
||||
|
||||
final loginTypes = await Matrix.of(context).client.getLoginFlows();
|
||||
if (loginTypes.flows
|
||||
.any((flow) => flow.type == AuthenticationTypes.password)) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamed(AppConfig.enableRegistration ? '/signup' : '/login');
|
||||
} else if (loginTypes.flows
|
||||
.any((flow) => flow.type == AuthenticationTypes.sso)) {
|
||||
final redirectUrl = kIsWeb
|
||||
? html.window.location.href
|
||||
: AppConfig.appOpenUrlScheme.toLowerCase() + '://sso';
|
||||
await launch(
|
||||
'${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}');
|
||||
}
|
||||
} catch (e) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text((e as Object).toLocalizedString(context))));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => HomeserverPickerUI(this);
|
||||
}
|
||||
42
lib/pages/image_viewer.dart
Normal file
42
lib/pages/image_viewer.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/pages/views/image_viewer_ui.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/event_extension.dart';
|
||||
|
||||
class ImageViewer extends StatefulWidget {
|
||||
final Event event;
|
||||
final void Function() onLoaded;
|
||||
|
||||
const ImageViewer(this.event, {Key key, this.onLoaded}) : super(key: key);
|
||||
|
||||
@override
|
||||
ImageViewerController createState() => ImageViewerController();
|
||||
}
|
||||
|
||||
class ImageViewerController extends State<ImageViewer> {
|
||||
/// Forward this image to another room.
|
||||
void forwardAction() {
|
||||
Matrix.of(context).shareContent = widget.event.content;
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
}
|
||||
|
||||
/// Open this file with a system call.
|
||||
void openFileAction() => widget.event.openFile(context, downloadOnly: true);
|
||||
|
||||
/// Go back if user swiped it away
|
||||
void onInteractionEnds(ScaleEndDetails endDetails) {
|
||||
if (PlatformInfos.usesTouchscreen == false) {
|
||||
if (endDetails.velocity.pixelsPerSecond.dy >
|
||||
MediaQuery.of(context).size.height * 1.50) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImageViewerUI(this);
|
||||
}
|
||||
120
lib/pages/invitation_selection.dart
Normal file
120
lib/pages/invitation_selection.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/views/invitation_selection_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../utils/localized_exception_extension.dart';
|
||||
|
||||
class InvitationSelection extends StatefulWidget {
|
||||
final String roomId;
|
||||
const InvitationSelection(this.roomId, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
InvitationSelectionController createState() =>
|
||||
InvitationSelectionController();
|
||||
}
|
||||
|
||||
class InvitationSelectionController extends State<InvitationSelection> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
String currentSearchTerm;
|
||||
bool loading = false;
|
||||
List<Profile> foundProfiles = [];
|
||||
Timer coolDown;
|
||||
|
||||
Future<List<User>> getContacts(BuildContext context) async {
|
||||
final client = Matrix.of(context).client;
|
||||
final room = client.getRoomById(widget.roomId);
|
||||
final participants = await room.requestParticipants();
|
||||
participants.removeWhere(
|
||||
(u) => ![Membership.join, Membership.invite].contains(u.membership),
|
||||
);
|
||||
final contacts = <User>[];
|
||||
final userMap = <String, bool>{};
|
||||
for (var i = 0; i < client.rooms.length; i++) {
|
||||
final roomUsers = client.rooms[i].getParticipants();
|
||||
|
||||
for (var j = 0; j < roomUsers.length; j++) {
|
||||
if (userMap[roomUsers[j].id] != true &&
|
||||
participants.indexWhere((u) => u.id == roomUsers[j].id) == -1) {
|
||||
contacts.add(roomUsers[j]);
|
||||
}
|
||||
userMap[roomUsers[j].id] = true;
|
||||
}
|
||||
}
|
||||
contacts.sort(
|
||||
(a, b) => a.calcDisplayname().toLowerCase().compareTo(
|
||||
b.calcDisplayname().toLowerCase(),
|
||||
),
|
||||
);
|
||||
return contacts;
|
||||
}
|
||||
|
||||
void inviteAction(BuildContext context, String id) async {
|
||||
final room = Matrix.of(context).client.getRoomById(widget.roomId);
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.invite(id),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(SnackBar(
|
||||
content: Text(L10n.of(context).contactHasBeenInvitedToTheGroup)));
|
||||
}
|
||||
}
|
||||
|
||||
void searchUserWithCoolDown(String text) async {
|
||||
coolDown?.cancel();
|
||||
coolDown = Timer(
|
||||
Duration(seconds: 1),
|
||||
() => searchUser(context, text),
|
||||
);
|
||||
}
|
||||
|
||||
void searchUser(BuildContext context, String text) async {
|
||||
coolDown?.cancel();
|
||||
if (text.isEmpty) {
|
||||
setState(() => foundProfiles = []);
|
||||
}
|
||||
currentSearchTerm = text;
|
||||
if (currentSearchTerm.isEmpty) return;
|
||||
if (loading) return;
|
||||
setState(() => loading = true);
|
||||
final matrix = Matrix.of(context);
|
||||
UserSearchResult response;
|
||||
try {
|
||||
response = await matrix.client.searchUserDirectory(text, limit: 10);
|
||||
} catch (e) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text((e as Object).toLocalizedString(context))));
|
||||
return;
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
setState(() {
|
||||
foundProfiles = List<Profile>.from(response.results);
|
||||
if (text.isValidMatrixId &&
|
||||
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
|
||||
setState(() => foundProfiles = [
|
||||
Profile.fromJson({'user_id': text}),
|
||||
]);
|
||||
}
|
||||
final participants = Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.roomId)
|
||||
.getParticipants()
|
||||
.where((user) =>
|
||||
[Membership.join, Membership.invite].contains(user.membership))
|
||||
.toList();
|
||||
foundProfiles.removeWhere((profile) =>
|
||||
participants.indexWhere((u) => u.id == profile.userId) != -1);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InvitationSelectionUI(this);
|
||||
}
|
||||
395
lib/pages/key_verification_dialog.dart
Normal file
395
lib/pages/key_verification_dialog.dart
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../widgets/adaptive_flat_button.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import '../utils/string_color.dart';
|
||||
import '../utils/beautify_string_extension.dart';
|
||||
|
||||
class KeyVerificationDialog extends StatefulWidget {
|
||||
Future<void> show(BuildContext context) => PlatformInfos.isCupertinoStyle
|
||||
? showCupertinoDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) => this,
|
||||
useRootNavigator: false,
|
||||
)
|
||||
: showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) => this,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
|
||||
final KeyVerification request;
|
||||
|
||||
KeyVerificationDialog({
|
||||
this.request,
|
||||
});
|
||||
|
||||
@override
|
||||
_KeyVerificationPageState createState() => _KeyVerificationPageState();
|
||||
}
|
||||
|
||||
class _KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
void Function() originalOnUpdate;
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
originalOnUpdate = widget.request.onUpdate;
|
||||
widget.request.onUpdate = () {
|
||||
if (originalOnUpdate != null) {
|
||||
originalOnUpdate();
|
||||
}
|
||||
setState(() => null);
|
||||
};
|
||||
widget.request.client.getProfileFromUserId(widget.request.userId).then((p) {
|
||||
profile = p;
|
||||
setState(() => null);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.request.onUpdate =
|
||||
originalOnUpdate; // don't want to get updates anymore
|
||||
if (![KeyVerificationState.error, KeyVerificationState.done]
|
||||
.contains(widget.request.state)) {
|
||||
widget.request.cancel('m.user');
|
||||
}
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Profile profile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
final buttons = <Widget>[];
|
||||
switch (widget.request.state) {
|
||||
case KeyVerificationState.askSSSS:
|
||||
// prompt the user for their ssss passphrase / key
|
||||
final textEditingController = TextEditingController();
|
||||
String input;
|
||||
final checkInput = () async {
|
||||
if (input == null || input.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final valid = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
// make sure the loading spinner shows before we test the keys
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
var valid = false;
|
||||
try {
|
||||
await widget.request.openSSSS(keyOrPassphrase: input);
|
||||
valid = true;
|
||||
} catch (_) {
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
});
|
||||
if (valid.error != null) {
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).incorrectPassphraseOrKey,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
}
|
||||
};
|
||||
body = Container(
|
||||
margin: EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).askSSSSSign,
|
||||
style: TextStyle(fontSize: 20)),
|
||||
Container(height: 10),
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
onSubmitted: (s) {
|
||||
input = s;
|
||||
checkInput();
|
||||
},
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).passphraseOrKey,
|
||||
prefixStyle: TextStyle(color: Theme.of(context).primaryColor),
|
||||
suffixStyle: TextStyle(color: Theme.of(context).primaryColor),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).submit,
|
||||
onPressed: () {
|
||||
input = textEditingController.text;
|
||||
checkInput();
|
||||
},
|
||||
));
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).skip,
|
||||
onPressed: () => widget.request.openSSSS(skip: true),
|
||||
));
|
||||
break;
|
||||
case KeyVerificationState.askAccept:
|
||||
body = Container(
|
||||
margin: EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
L10n.of(context).askVerificationRequest(widget.request.userId),
|
||||
style: TextStyle(fontSize: 20)),
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).accept,
|
||||
onPressed: () => widget.request.acceptVerification(),
|
||||
));
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).reject,
|
||||
onPressed: () {
|
||||
widget.request.rejectVerification().then((_) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
});
|
||||
},
|
||||
));
|
||||
break;
|
||||
case KeyVerificationState.waitingAccept:
|
||||
body = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PlatformInfos.isCupertinoStyle
|
||||
? CupertinoActivityIndicator()
|
||||
: CircularProgressIndicator(),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
L10n.of(context).waitingPartnerAcceptRequest,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
final key = widget.request.client.userDeviceKeys[widget.request.userId]
|
||||
.deviceKeys[widget.request.deviceId];
|
||||
if (key != null) {
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).verifyManual,
|
||||
onPressed: () async {
|
||||
final result = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context).verifyManual,
|
||||
message: key.ed25519Key.beautified,
|
||||
);
|
||||
if (result == OkCancelResult.ok) {
|
||||
await key.setVerified(true);
|
||||
}
|
||||
await widget.request.cancel();
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
break;
|
||||
case KeyVerificationState.askSas:
|
||||
TextSpan compareWidget;
|
||||
// maybe add a button to switch between the two and only determine default
|
||||
// view for if "emoji" is a present sasType or not?
|
||||
String compareText;
|
||||
if (widget.request.sasTypes.contains('emoji')) {
|
||||
compareText = L10n.of(context).compareEmojiMatch;
|
||||
compareWidget = TextSpan(
|
||||
children: widget.request.sasEmojis
|
||||
.map((e) => WidgetSpan(child: _Emoji(e)))
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
compareText = L10n.of(context).compareNumbersMatch;
|
||||
final numbers = widget.request.sasNumbers;
|
||||
final numbstr = '${numbers[0]}-${numbers[1]}-${numbers[2]}';
|
||||
compareWidget =
|
||||
TextSpan(text: numbstr, style: TextStyle(fontSize: 40));
|
||||
}
|
||||
body = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: Text(
|
||||
compareText,
|
||||
style: TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text.rich(
|
||||
compareWidget,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
textColor: Colors.red,
|
||||
label: L10n.of(context).theyDontMatch,
|
||||
onPressed: () => widget.request.rejectSas(),
|
||||
));
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).theyMatch,
|
||||
onPressed: () => widget.request.acceptSas(),
|
||||
));
|
||||
break;
|
||||
case KeyVerificationState.waitingSas:
|
||||
final acceptText = widget.request.sasTypes.contains('emoji')
|
||||
? L10n.of(context).waitingPartnerEmoji
|
||||
: L10n.of(context).waitingPartnerNumbers;
|
||||
body = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PlatformInfos.isCupertinoStyle
|
||||
? CupertinoActivityIndicator()
|
||||
: CircularProgressIndicator(),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
acceptText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
break;
|
||||
case KeyVerificationState.done:
|
||||
body = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.check_circle_outlined, color: Colors.green, size: 200.0),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
L10n.of(context).verifySuccess,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).close,
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
));
|
||||
break;
|
||||
case KeyVerificationState.error:
|
||||
body = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.cancel, color: Colors.red, size: 200.0),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
'Error ${widget.request.canceledCode}: ${widget.request.canceledReason}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context).close,
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
));
|
||||
break;
|
||||
}
|
||||
body ??= Text('ERROR: Unknown state ' + widget.request.state.toString());
|
||||
final otherName = profile?.displayname ?? widget.request.userId;
|
||||
var bottom;
|
||||
if (widget.request.deviceId != null) {
|
||||
final deviceName = widget
|
||||
.request
|
||||
.client
|
||||
.userDeviceKeys[widget.request.userId]
|
||||
?.deviceKeys[widget.request.deviceId]
|
||||
?.deviceDisplayName ??
|
||||
'';
|
||||
bottom = Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text('$deviceName (${widget.request.deviceId})',
|
||||
style: TextStyle(color: Theme.of(context).textTheme.caption.color)),
|
||||
);
|
||||
}
|
||||
final userNameTitle = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
otherName,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: otherName.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (otherName != widget.request.userId)
|
||||
Text(
|
||||
' - ' + widget.request.userId,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
final title = Text(L10n.of(context).verifyTitle);
|
||||
final content = Scrollbar(
|
||||
isAlwaysShown: true,
|
||||
controller: _scrollController,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (PlatformInfos.isCupertinoStyle) ...[
|
||||
SizedBox(height: 8),
|
||||
Center(child: userNameTitle),
|
||||
SizedBox(height: 12),
|
||||
],
|
||||
body,
|
||||
if (bottom != null) bottom,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (PlatformInfos.isCupertinoStyle) {
|
||||
return CupertinoAlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: buttons,
|
||||
);
|
||||
}
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: buttons,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Emoji extends StatelessWidget {
|
||||
final KeyVerificationEmoji emoji;
|
||||
|
||||
_Emoji(this.emoji);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(emoji.emoji, style: TextStyle(fontSize: 50)),
|
||||
Text(emoji.name),
|
||||
Container(height: 10, width: 5),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
44
lib/pages/new_group.dart
Normal file
44
lib/pages/new_group.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart' as sdk;
|
||||
import 'package:fluffychat/pages/views/new_group_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewGroup extends StatefulWidget {
|
||||
@override
|
||||
NewGroupController createState() => NewGroupController();
|
||||
}
|
||||
|
||||
class NewGroupController extends State<NewGroup> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
bool publicGroup = false;
|
||||
|
||||
void setPublicGroup(bool b) => setState(() => publicGroup = b);
|
||||
|
||||
void submitAction([_]) async {
|
||||
final matrix = Matrix.of(context);
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => matrix.client.createRoom(
|
||||
preset: publicGroup
|
||||
? sdk.CreateRoomPreset.public_chat
|
||||
: sdk.CreateRoomPreset.private_chat,
|
||||
visibility: publicGroup ? sdk.Visibility.public : null,
|
||||
roomAliasName: publicGroup && controller.text.isNotEmpty
|
||||
? controller.text.trim().toLowerCase().replaceAll(' ', '_')
|
||||
: null,
|
||||
name: controller.text.isNotEmpty ? controller.text : null,
|
||||
),
|
||||
);
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
if (roomID != null) {
|
||||
await AdaptivePageLayout.of(context).pushNamed('/rooms/${roomID.result}');
|
||||
await AdaptivePageLayout.of(context)
|
||||
.pushNamed('/rooms/${roomID.result}/invite');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewGroupUI(this);
|
||||
}
|
||||
116
lib/pages/new_private_chat.dart
Normal file
116
lib/pages/new_private_chat.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/pages/views/new_private_chat_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class NewPrivateChat extends StatefulWidget {
|
||||
@override
|
||||
NewPrivateChatController createState() => NewPrivateChatController();
|
||||
}
|
||||
|
||||
class NewPrivateChatController extends State<NewPrivateChat> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
bool loading = false;
|
||||
String currentSearchTerm;
|
||||
List<Profile> foundProfiles = [];
|
||||
Timer coolDown;
|
||||
Profile get foundProfile =>
|
||||
foundProfiles.firstWhere((user) => user.userId == '@$currentSearchTerm',
|
||||
orElse: () => null);
|
||||
bool get correctMxId =>
|
||||
foundProfiles
|
||||
.indexWhere((user) => user.userId == '@$currentSearchTerm') !=
|
||||
-1;
|
||||
|
||||
void submitAction([_]) async {
|
||||
controller.text = controller.text.replaceAll('@', '').trim();
|
||||
if (controller.text.isEmpty) return;
|
||||
if (!formKey.currentState.validate()) return;
|
||||
final matrix = Matrix.of(context);
|
||||
|
||||
if ('@' + controller.text == matrix.client.userID) return;
|
||||
|
||||
final user = User(
|
||||
'@' + controller.text,
|
||||
room: Room(id: '', client: matrix.client),
|
||||
);
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => user.startDirectChat(),
|
||||
);
|
||||
|
||||
if (roomID.error == null) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.popAndPushNamed('/rooms/${roomID.result}');
|
||||
}
|
||||
}
|
||||
|
||||
void searchUserWithCoolDown([_]) async {
|
||||
coolDown?.cancel();
|
||||
coolDown = Timer(
|
||||
Duration(milliseconds: 500),
|
||||
() => searchUser(controller.text),
|
||||
);
|
||||
}
|
||||
|
||||
void searchUser(String text) async {
|
||||
if (text.isEmpty) {
|
||||
setState(() {
|
||||
foundProfiles = [];
|
||||
});
|
||||
}
|
||||
currentSearchTerm = text;
|
||||
if (currentSearchTerm.isEmpty) return;
|
||||
if (loading) return;
|
||||
setState(() => loading = true);
|
||||
final matrix = Matrix.of(context);
|
||||
UserSearchResult response;
|
||||
try {
|
||||
response = await matrix.client.searchUserDirectory(text, limit: 10);
|
||||
} catch (_) {}
|
||||
setState(() => loading = false);
|
||||
if (response?.results?.isEmpty ?? true) return;
|
||||
setState(() {
|
||||
foundProfiles = List<Profile>.from(response.results);
|
||||
});
|
||||
}
|
||||
|
||||
String validateForm(String value) {
|
||||
if (value.isEmpty) {
|
||||
return L10n.of(context).pleaseEnterAMatrixIdentifier;
|
||||
}
|
||||
final matrix = Matrix.of(context);
|
||||
final mxid = '@' + controller.text.trim();
|
||||
if (mxid == matrix.client.userID) {
|
||||
return L10n.of(context).youCannotInviteYourself;
|
||||
}
|
||||
if (!mxid.contains('@')) {
|
||||
return L10n.of(context).makeSureTheIdentifierIsValid;
|
||||
}
|
||||
if (!mxid.contains(':')) {
|
||||
return L10n.of(context).makeSureTheIdentifierIsValid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void inviteAction() => FluffyShare.share(
|
||||
L10n.of(context).inviteText(Matrix.of(context).client.userID,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}'),
|
||||
context,
|
||||
);
|
||||
|
||||
void pickUser(Profile foundProfile) => setState(
|
||||
() => controller.text =
|
||||
currentSearchTerm = foundProfile.userId.substring(1),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewPrivateChatUI(this);
|
||||
}
|
||||
97
lib/pages/permission_slider_dialog.dart
Normal file
97
lib/pages/permission_slider_dialog.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:fluffychat/widgets/adaptive_flat_button.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class PermissionSliderDialog extends StatefulWidget {
|
||||
const PermissionSliderDialog({
|
||||
Key key,
|
||||
this.initialPermission = 0,
|
||||
}) : super(key: key);
|
||||
|
||||
Future<int> show(BuildContext context) => PlatformInfos.isCupertinoStyle
|
||||
? showCupertinoDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => this,
|
||||
useRootNavigator: false,
|
||||
)
|
||||
: showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => this,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
|
||||
final int initialPermission;
|
||||
@override
|
||||
_PermissionSliderDialogState createState() => _PermissionSliderDialogState();
|
||||
}
|
||||
|
||||
class _PermissionSliderDialogState extends State<PermissionSliderDialog> {
|
||||
int _permission;
|
||||
@override
|
||||
void initState() {
|
||||
_permission = widget.initialPermission;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final slider = PlatformInfos.isCupertinoStyle
|
||||
? CupertinoSlider(
|
||||
value: _permission.toDouble(),
|
||||
onChanged: (d) => setState(() => _permission = d.round()),
|
||||
max: 100.0,
|
||||
min: 0.0,
|
||||
)
|
||||
: Slider(
|
||||
value: _permission.toDouble(),
|
||||
onChanged: (d) => setState(() => _permission = d.round()),
|
||||
max: 100.0,
|
||||
min: 0.0,
|
||||
);
|
||||
final title = Text(
|
||||
L10n.of(context).setPermissionsLevel,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Level: ' +
|
||||
(_permission == 100
|
||||
? '$_permission (${L10n.of(context).admin})'
|
||||
: _permission >= 50
|
||||
? '$_permission (${L10n.of(context).moderator})'
|
||||
: _permission.toString())),
|
||||
Container(
|
||||
height: 56,
|
||||
child: slider,
|
||||
),
|
||||
],
|
||||
);
|
||||
final buttons = [
|
||||
AdaptiveFlatButton(
|
||||
label: L10n.of(context).cancel,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<int>(null),
|
||||
),
|
||||
AdaptiveFlatButton(
|
||||
label: L10n.of(context).confirm,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<int>(_permission),
|
||||
),
|
||||
];
|
||||
if (PlatformInfos.isCupertinoStyle) {
|
||||
return CupertinoAlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: buttons,
|
||||
);
|
||||
}
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: buttons,
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/pages/recording_dialog.dart
Normal file
128
lib/pages/recording_dialog.dart
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/utils/sentry_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
class RecordingDialog extends StatefulWidget {
|
||||
static const String recordingFileType = 'aac';
|
||||
const RecordingDialog({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_RecordingDialogState createState() => _RecordingDialogState();
|
||||
}
|
||||
|
||||
class _RecordingDialogState extends State<RecordingDialog> {
|
||||
Timer _recorderSubscription;
|
||||
Duration _duration = Duration.zero;
|
||||
|
||||
bool error = false;
|
||||
String _recordedPath;
|
||||
|
||||
void startRecording() async {
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
_recordedPath =
|
||||
'${tempDir.path}/recording${DateTime.now().microsecondsSinceEpoch}.${RecordingDialog.recordingFileType}';
|
||||
|
||||
final result = await Record.hasPermission();
|
||||
if (result != true) {
|
||||
setState(() => error = true);
|
||||
return;
|
||||
}
|
||||
await Record.start(path: _recordedPath, encoder: AudioEncoder.AAC);
|
||||
setState(() => _duration = Duration.zero);
|
||||
_recorderSubscription?.cancel();
|
||||
_recorderSubscription = Timer.periodic(Duration(seconds: 1),
|
||||
(_) => setState(() => _duration += Duration(seconds: 1)));
|
||||
} catch (e, s) {
|
||||
SentryController.captureException(e, s);
|
||||
setState(() => error = true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
startRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_recorderSubscription?.cancel();
|
||||
Record.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const maxDecibalWidth = 64.0;
|
||||
final decibalWidth =
|
||||
((_duration.inSeconds % 2) + 1) * (maxDecibalWidth / 4).toDouble();
|
||||
final time =
|
||||
'${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}';
|
||||
|
||||
return AlertDialog(
|
||||
content: error
|
||||
? Text(L10n.of(context).oopsSomethingWentWrong)
|
||||
: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: maxDecibalWidth,
|
||||
height: maxDecibalWidth,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(seconds: 1),
|
||||
width: decibalWidth,
|
||||
height: decibalWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(decibalWidth),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${L10n.of(context).recording}: $time',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
child: Text(
|
||||
L10n.of(context).cancel.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyText2.color.withAlpha(150),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (error != true)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
_recorderSubscription?.cancel();
|
||||
await Record.stop();
|
||||
Navigator.of(context, rootNavigator: false)
|
||||
.pop<String>(_recordedPath);
|
||||
},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).send.toUpperCase()),
|
||||
SizedBox(width: 4),
|
||||
Icon(Icons.send_outlined, size: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
143
lib/pages/search.dart
Normal file
143
lib/pages/search.dart
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
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/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'views/search_ui.dart';
|
||||
|
||||
class Search extends StatefulWidget {
|
||||
final String alias;
|
||||
|
||||
const Search({Key key, this.alias}) : super(key: key);
|
||||
|
||||
@override
|
||||
SearchController createState() => SearchController();
|
||||
}
|
||||
|
||||
class SearchController extends State<Search> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
Future<PublicRoomsResponse> publicRoomsResponse;
|
||||
String lastServer;
|
||||
Timer _coolDown;
|
||||
String genericSearchTerm;
|
||||
|
||||
void search(String query) async {
|
||||
setState(() => null);
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(
|
||||
Duration(milliseconds: 500),
|
||||
() => setState(() {
|
||||
genericSearchTerm = query;
|
||||
publicRoomsResponse = null;
|
||||
searchUser(context, controller.text);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
.joinRoom(alias?.isNotEmpty ?? false ? alias : roomId);
|
||||
await Matrix.of(context)
|
||||
.client
|
||||
.onRoomUpdate
|
||||
.stream
|
||||
.firstWhere((r) => r.id == newRoomId);
|
||||
return newRoomId;
|
||||
}
|
||||
|
||||
void joinGroupAction(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,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
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() async {
|
||||
final newServer = await showTextInputDialog(
|
||||
title: L10n.of(context).changeTheHomeserver,
|
||||
context: context,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
prefixText: 'https://',
|
||||
hintText: Matrix.of(context).client.homeserver.host,
|
||||
initialText: server,
|
||||
keyboardType: TextInputType.url,
|
||||
)
|
||||
]);
|
||||
if (newServer == null) return;
|
||||
setState(() {
|
||||
server = newServer.single;
|
||||
});
|
||||
}
|
||||
|
||||
String currentSearchTerm;
|
||||
List<Profile> foundProfiles = [];
|
||||
|
||||
void searchUser(BuildContext context, String text) async {
|
||||
if (text.isEmpty) {
|
||||
setState(() {
|
||||
foundProfiles = [];
|
||||
});
|
||||
}
|
||||
currentSearchTerm = text;
|
||||
if (currentSearchTerm.isEmpty) return;
|
||||
final matrix = Matrix.of(context);
|
||||
UserSearchResult response;
|
||||
try {
|
||||
response = await matrix.client.searchUserDirectory(text, limit: 10);
|
||||
} catch (_) {}
|
||||
foundProfiles = List<Profile>.from(response?.results ?? []);
|
||||
if (foundProfiles.isEmpty && text.isValidMatrixId && text.sigil == '@') {
|
||||
foundProfiles.add(Profile.fromJson({
|
||||
'displayname': text.localpart,
|
||||
'user_id': text,
|
||||
}));
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
genericSearchTerm = widget.alias;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SearchUI(this);
|
||||
}
|
||||
103
lib/pages/send_file_dialog.dart
Normal file
103
lib/pages/send_file_dialog.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import '../utils/matrix_file_extension.dart';
|
||||
import '../utils/room_send_file_extension.dart';
|
||||
import '../utils/resize_image.dart';
|
||||
|
||||
class SendFileDialog extends StatefulWidget {
|
||||
final Room room;
|
||||
final MatrixFile file;
|
||||
|
||||
const SendFileDialog({
|
||||
this.room,
|
||||
this.file,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SendFileDialogState createState() => _SendFileDialogState();
|
||||
}
|
||||
|
||||
class _SendFileDialogState extends State<SendFileDialog> {
|
||||
bool origImage = false;
|
||||
bool _isSending = false;
|
||||
Future<void> _send() async {
|
||||
var file = widget.file;
|
||||
if (file is MatrixImageFile && !origImage) {
|
||||
try {
|
||||
file = await resizeImage(file, max: 1600);
|
||||
} catch (e) {
|
||||
// couldn't resize
|
||||
}
|
||||
}
|
||||
await widget.room.sendFileEventWithThumbnail(file);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var sendStr = L10n.of(context).sendFile;
|
||||
if (widget.file is MatrixImageFile) {
|
||||
sendStr = L10n.of(context).sendImage;
|
||||
} else if (widget.file is MatrixAudioFile) {
|
||||
sendStr = L10n.of(context).sendAudio;
|
||||
} else if (widget.file is MatrixVideoFile) {
|
||||
sendStr = L10n.of(context).sendVideo;
|
||||
}
|
||||
Widget contentWidget;
|
||||
if (widget.file is MatrixImageFile) {
|
||||
contentWidget = Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Flexible(
|
||||
child: Image.memory(
|
||||
widget.file.bytes,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Text(widget.file.name),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: origImage,
|
||||
onChanged: (v) => setState(() => origImage = v),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => setState(() => origImage = !origImage),
|
||||
child: Text(L10n.of(context).sendOriginal +
|
||||
' (${widget.file.sizeString})'),
|
||||
),
|
||||
],
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
contentWidget = Text('${widget.file.name} (${widget.file.sizeString})');
|
||||
}
|
||||
return AlertDialog(
|
||||
title: Text(sendStr),
|
||||
content: contentWidget,
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// just close the dialog
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
},
|
||||
child: Text(L10n.of(context).cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isSending
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
_isSending = true;
|
||||
});
|
||||
await showFutureLoadingDialog(
|
||||
context: context, future: () => _send());
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
},
|
||||
child: Text(L10n.of(context).send),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
378
lib/pages/settings.dart
Normal file
378
lib/pages/settings.dart
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
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:file_picker_cross/file_picker_cross.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/utils/sentry_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_app_lock/flutter_app_lock.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'views/settings_ui.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'bootstrap_dialog.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../config/setting_keys.dart';
|
||||
|
||||
class Settings extends StatefulWidget {
|
||||
@override
|
||||
SettingsController createState() => SettingsController();
|
||||
}
|
||||
|
||||
class SettingsController extends State<Settings> {
|
||||
Future<dynamic> profileFuture;
|
||||
dynamic profile;
|
||||
Future<bool> crossSigningCachedFuture;
|
||||
bool crossSigningCached;
|
||||
Future<bool> megolmBackupCachedFuture;
|
||||
bool megolmBackupCached;
|
||||
|
||||
void logoutAction() async {
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSureYouWantToLogout,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.cancel) {
|
||||
return;
|
||||
}
|
||||
final matrix = Matrix.of(context);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => matrix.client.logout(),
|
||||
);
|
||||
}
|
||||
|
||||
void changePasswordAccountAction() async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).changePassword,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).pleaseEnterYourPassword,
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).chooseAStrongPassword,
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.changePassword(input.last, oldPassword: input.first),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)));
|
||||
}
|
||||
}
|
||||
|
||||
void deleteAccountAction() async {
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).warning,
|
||||
message: L10n.of(context).deactivateAccountWarning,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.cancel) {
|
||||
return;
|
||||
}
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) ==
|
||||
OkCancelResult.cancel) {
|
||||
return;
|
||||
}
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).pleaseEnterYourPassword,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
obscureText: true,
|
||||
hintText: '******',
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.deactivateAccount(
|
||||
auth: AuthenticationPassword(
|
||||
password: input.single,
|
||||
user: Matrix.of(context).client.userID,
|
||||
identifier: AuthenticationUserIdentifier(
|
||||
user: Matrix.of(context).client.userID),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setJitsiInstanceAction() async {
|
||||
const prefix = 'https://';
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).editJitsiInstance,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
initialText: AppConfig.jitsiInstance.replaceFirst(prefix, ''),
|
||||
prefixText: prefix,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
var jitsi = prefix + input.single;
|
||||
if (!jitsi.endsWith('/')) {
|
||||
jitsi += '/';
|
||||
}
|
||||
final matrix = Matrix.of(context);
|
||||
await matrix.store.setItem(SettingKeys.jitsiInstance, jitsi);
|
||||
AppConfig.jitsiInstance = jitsi;
|
||||
}
|
||||
|
||||
void setDisplaynameAction() async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).editDisplayname,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
initialText: profile?.displayname ??
|
||||
Matrix.of(context).client.userID.localpart,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final matrix = Matrix.of(context);
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
matrix.client.setDisplayName(matrix.client.userID, input.single),
|
||||
);
|
||||
if (success.error == null) {
|
||||
setState(() {
|
||||
profileFuture = null;
|
||||
profile = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void setAvatarAction() 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 showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => matrix.client.setAvatar(file),
|
||||
);
|
||||
if (success.error == null) {
|
||||
setState(() {
|
||||
profileFuture = null;
|
||||
profile = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> requestSSSSCache() async {
|
||||
final handle = Matrix.of(context).client.encryption.ssss.open();
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).askSSSSCache,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).passphraseOrKey,
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (input != null) {
|
||||
final valid = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
// make sure the loading spinner shows before we test the keys
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
var valid = false;
|
||||
try {
|
||||
await handle.unlock(recoveryKey: input.single);
|
||||
valid = true;
|
||||
} catch (e, s) {
|
||||
SentryController.captureException(e, s);
|
||||
}
|
||||
return valid;
|
||||
});
|
||||
|
||||
if (valid.result == true) {
|
||||
await handle.maybeCacheAll();
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).cachedKeys,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
setState(() {
|
||||
crossSigningCachedFuture = null;
|
||||
crossSigningCached = null;
|
||||
megolmBackupCachedFuture = null;
|
||||
megolmBackupCached = null;
|
||||
});
|
||||
} else {
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).incorrectPassphraseOrKey,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setAppLockAction() async {
|
||||
final currentLock =
|
||||
await FlutterSecureStorage().read(key: SettingKeys.appLockKey);
|
||||
if (currentLock?.isNotEmpty ?? false) {
|
||||
await AppLock.of(context).showLockScreen();
|
||||
}
|
||||
final newLock = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).pleaseChooseAPasscode,
|
||||
message: L10n.of(context).pleaseEnter4Digits,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
validator: (text) {
|
||||
if (text.isEmpty || (text.length == 4 && int.tryParse(text) >= 0)) {
|
||||
return null;
|
||||
}
|
||||
return L10n.of(context).pleaseEnter4Digits;
|
||||
},
|
||||
keyboardType: TextInputType.number,
|
||||
obscureText: true,
|
||||
maxLines: 1,
|
||||
minLines: 1,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (newLock != null) {
|
||||
await FlutterSecureStorage()
|
||||
.write(key: SettingKeys.appLockKey, value: newLock.single);
|
||||
if (newLock.single.isEmpty) {
|
||||
AppLock.of(context).disable();
|
||||
} else {
|
||||
AppLock.of(context).enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void bootstrapSettingsAction() async {
|
||||
if (await Matrix.of(context).client.encryption.keyManager.isCached()) {
|
||||
if (OkCancelResult.ok ==
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).keysCached,
|
||||
message: L10n.of(context).wipeChatBackup,
|
||||
isDestructiveAction: true,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
)) {
|
||||
await BootstrapDialog(
|
||||
client: Matrix.of(context).client,
|
||||
wipe: true,
|
||||
).show(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await BootstrapDialog(
|
||||
client: Matrix.of(context).client,
|
||||
).show(context);
|
||||
}
|
||||
|
||||
void firstRunBootstrapAction() async {
|
||||
await BootstrapDialog(
|
||||
client: Matrix.of(context).client,
|
||||
).show(context);
|
||||
AdaptivePageLayout.of(context).popUntilIsFirst();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
profileFuture ??= client.ownProfile.then((p) {
|
||||
if (mounted) setState(() => profile = p);
|
||||
return p;
|
||||
});
|
||||
if (client.encryption != null) {
|
||||
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 SettingsUI(this);
|
||||
}
|
||||
}
|
||||
90
lib/pages/settings_3pid.dart
Normal file
90
lib/pages/settings_3pid.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'views/settings_3pid_ui.dart';
|
||||
|
||||
class Settings3Pid extends StatefulWidget {
|
||||
static int sendAttempt = 0;
|
||||
|
||||
@override
|
||||
Settings3PidController createState() => Settings3PidController();
|
||||
}
|
||||
|
||||
class Settings3PidController extends State<Settings3Pid> {
|
||||
void add3PidAction() async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).enterAnEmailAddress,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).enterAnEmailAddress,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final response = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.requestEmailToken(
|
||||
input.single,
|
||||
clientSecret,
|
||||
Settings3Pid.sendAttempt++,
|
||||
),
|
||||
);
|
||||
if (response.error != null) return;
|
||||
final ok = await showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).weSentYouAnEmail,
|
||||
message: L10n.of(context).pleaseClickOnLink,
|
||||
okLabel: L10n.of(context).iHaveClickedOnLink,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
if (ok == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.uiaRequestBackground(
|
||||
(auth) => Matrix.of(context).client.add3PID(
|
||||
clientSecret,
|
||||
response.result.sid,
|
||||
auth: auth,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (success.error != null) return;
|
||||
setState(() => request = null);
|
||||
}
|
||||
|
||||
Future<List<ThirdPartyIdentifier>> request;
|
||||
|
||||
void delete3Pid(ThirdPartyIdentifier identifier) async {
|
||||
if (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
) !=
|
||||
OkCancelResult.ok) {
|
||||
return;
|
||||
}
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.delete3pidFromAccount(
|
||||
identifier.address,
|
||||
identifier.medium,
|
||||
));
|
||||
if (success.error != null) return;
|
||||
setState(() => request = null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Settings3PidUI(this);
|
||||
}
|
||||
300
lib/pages/settings_emotes.dart
Normal file
300
lib/pages/settings_emotes.dart
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'views/settings_emotes_ui.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class EmotesSettings extends StatefulWidget {
|
||||
final Room room;
|
||||
final String stateKey;
|
||||
|
||||
EmotesSettings({this.room, this.stateKey});
|
||||
|
||||
@override
|
||||
EmotesSettingsController createState() => EmotesSettingsController();
|
||||
}
|
||||
|
||||
class EmoteEntry {
|
||||
String emote;
|
||||
String mxc;
|
||||
EmoteEntry({this.emote, this.mxc});
|
||||
|
||||
String get emoteClean => emote.substring(1, emote.length - 1);
|
||||
}
|
||||
|
||||
class EmotesSettingsController extends State<EmotesSettings> {
|
||||
List<EmoteEntry> emotes;
|
||||
bool showSave = false;
|
||||
TextEditingController newEmoteController = TextEditingController();
|
||||
TextEditingController newMxcController = TextEditingController();
|
||||
|
||||
Future<void> _save(BuildContext context) async {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
final client = Matrix.of(context).client;
|
||||
// be sure to preserve any data not in "short"
|
||||
Map<String, dynamic> content;
|
||||
if (widget.room != null) {
|
||||
content = widget.room
|
||||
.getState('im.ponies.room_emotes', widget.stateKey ?? '')
|
||||
?.content ??
|
||||
<String, dynamic>{};
|
||||
} else {
|
||||
content = client.accountData['im.ponies.user_emotes']?.content ??
|
||||
<String, dynamic>{};
|
||||
}
|
||||
if (!(content['emoticons'] is Map)) {
|
||||
content['emoticons'] = <String, dynamic>{};
|
||||
}
|
||||
// add / update changed emotes
|
||||
final allowedShortcodes = <String>{};
|
||||
for (final emote in emotes) {
|
||||
allowedShortcodes.add(emote.emote);
|
||||
if (!(content['emoticons'][emote.emote] is Map)) {
|
||||
content['emoticons'][emote.emote] = <String, dynamic>{};
|
||||
}
|
||||
content['emoticons'][emote.emote]['url'] = emote.mxc;
|
||||
}
|
||||
// remove emotes no more needed
|
||||
// we make the iterator .toList() here so that we don't get into trouble modifying the very
|
||||
// thing we are iterating over
|
||||
for (final shortcode in content['emoticons'].keys.toList()) {
|
||||
if (!allowedShortcodes.contains(shortcode)) {
|
||||
content['emoticons'].remove(shortcode);
|
||||
}
|
||||
}
|
||||
// remove the old "short" key
|
||||
content.remove('short');
|
||||
if (widget.room != null) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => client.setRoomStateWithKey(widget.room.id,
|
||||
'im.ponies.room_emotes', content, widget.stateKey ?? ''),
|
||||
);
|
||||
} else {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => client.setAccountData(
|
||||
client.userID, 'im.ponies.user_emotes', content),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setIsGloballyActive(bool active) async {
|
||||
if (widget.room == null) {
|
||||
return;
|
||||
}
|
||||
final client = Matrix.of(context).client;
|
||||
final content = client.accountData['im.ponies.emote_rooms']?.content ??
|
||||
<String, dynamic>{};
|
||||
if (active) {
|
||||
if (!(content['rooms'] is Map)) {
|
||||
content['rooms'] = <String, dynamic>{};
|
||||
}
|
||||
if (!(content['rooms'][widget.room.id] is Map)) {
|
||||
content['rooms'][widget.room.id] = <String, dynamic>{};
|
||||
}
|
||||
if (!(content['rooms'][widget.room.id][widget.stateKey ?? ''] is Map)) {
|
||||
content['rooms'][widget.room.id]
|
||||
[widget.stateKey ?? ''] = <String, dynamic>{};
|
||||
}
|
||||
} else if (content['rooms'] is Map &&
|
||||
content['rooms'][widget.room.id] is Map) {
|
||||
content['rooms'][widget.room.id].remove(widget.stateKey ?? '');
|
||||
}
|
||||
// and save
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => client.setAccountData(
|
||||
client.userID, 'im.ponies.emote_rooms', content),
|
||||
);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
void removeEmoteAction(EmoteEntry emote) => setState(() {
|
||||
emotes.removeWhere((e) => e.emote == emote.emote);
|
||||
showSave = true;
|
||||
});
|
||||
|
||||
void submitEmoteAction(
|
||||
String s,
|
||||
EmoteEntry emote,
|
||||
TextEditingController controller,
|
||||
) {
|
||||
final emoteCode = ':$s:';
|
||||
if (emotes.indexWhere((e) => e.emote == emoteCode && e.mxc != emote.mxc) !=
|
||||
-1) {
|
||||
controller.text = emote.emoteClean;
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).emoteExists,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!RegExp(r'^:[-\w]+:$').hasMatch(emoteCode)) {
|
||||
controller.text = emote.emoteClean;
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).emoteInvalid,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
emote.emote = emoteCode;
|
||||
showSave = true;
|
||||
});
|
||||
}
|
||||
|
||||
bool isGloballyActive(Client client) =>
|
||||
widget.room != null &&
|
||||
client.accountData['im.ponies.emote_rooms']?.content is Map &&
|
||||
client.accountData['im.ponies.emote_rooms'].content['rooms'] is Map &&
|
||||
client.accountData['im.ponies.emote_rooms'].content['rooms']
|
||||
[widget.room.id] is Map &&
|
||||
client.accountData['im.ponies.emote_rooms'].content['rooms']
|
||||
[widget.room.id][widget.stateKey ?? ''] is Map;
|
||||
|
||||
bool get readonly => widget.room == null
|
||||
? false
|
||||
: !(widget.room.canSendEvent('im.ponies.room_emotes'));
|
||||
|
||||
void saveAction() async {
|
||||
await _save(context);
|
||||
setState(() {
|
||||
showSave = false;
|
||||
});
|
||||
}
|
||||
|
||||
void addEmoteAction() async {
|
||||
if (newEmoteController.text == null ||
|
||||
newEmoteController.text.isEmpty ||
|
||||
newMxcController.text == null ||
|
||||
newMxcController.text.isEmpty) {
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).emoteWarnNeedToPick,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final emoteCode = ':${newEmoteController.text}:';
|
||||
final mxc = newMxcController.text;
|
||||
if (emotes.indexWhere((e) => e.emote == emoteCode && e.mxc != mxc) != -1) {
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).emoteExists,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!RegExp(r'^:[-\w]+:$').hasMatch(emoteCode)) {
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).emoteInvalid,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
emotes.add(EmoteEntry(emote: emoteCode, mxc: mxc));
|
||||
await _save(context);
|
||||
setState(() {
|
||||
newEmoteController.text = '';
|
||||
newMxcController.text = '';
|
||||
showSave = false;
|
||||
});
|
||||
}
|
||||
|
||||
void emoteImagePickerAction(TextEditingController controller) async {
|
||||
if (kIsWeb) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).notSupportedInWeb)));
|
||||
return;
|
||||
}
|
||||
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 uploadResp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).client.uploadContent(file.bytes, file.name),
|
||||
);
|
||||
if (uploadResp.error == null) {
|
||||
setState(() {
|
||||
controller.text = uploadResp.result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (emotes == null) {
|
||||
emotes = <EmoteEntry>[];
|
||||
Map<String, dynamic> emoteSource;
|
||||
if (widget.room != null) {
|
||||
emoteSource = widget.room
|
||||
.getState('im.ponies.room_emotes', widget.stateKey ?? '')
|
||||
?.content;
|
||||
} else {
|
||||
emoteSource = Matrix.of(context)
|
||||
.client
|
||||
.accountData['im.ponies.user_emotes']
|
||||
?.content;
|
||||
}
|
||||
if (emoteSource != null) {
|
||||
if (emoteSource['emoticons'] is Map) {
|
||||
emoteSource['emoticons'].forEach((key, value) {
|
||||
if (key is String &&
|
||||
value is Map &&
|
||||
value['url'] is String &&
|
||||
value['url'].startsWith('mxc://')) {
|
||||
emotes.add(EmoteEntry(emote: key, mxc: value['url']));
|
||||
}
|
||||
});
|
||||
} else if (emoteSource['short'] is Map) {
|
||||
emoteSource['short'].forEach((key, value) {
|
||||
if (key is String &&
|
||||
value is String &&
|
||||
value.startsWith('mxc://')) {
|
||||
emotes.add(EmoteEntry(emote: key, mxc: value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return EmotesSettingsUI(this);
|
||||
}
|
||||
}
|
||||
40
lib/pages/settings_ignore_list.dart
Normal file
40
lib/pages/settings_ignore_list.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'views/settings_ignore_list_ui.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class SettingsIgnoreList extends StatefulWidget {
|
||||
final String initialUserId;
|
||||
|
||||
SettingsIgnoreList({Key key, this.initialUserId}) : super(key: key);
|
||||
|
||||
@override
|
||||
SettingsIgnoreListController createState() => SettingsIgnoreListController();
|
||||
}
|
||||
|
||||
class SettingsIgnoreListController extends State<SettingsIgnoreList> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialUserId != null) {
|
||||
controller.text = widget.initialUserId.replaceAll('@', '');
|
||||
}
|
||||
}
|
||||
|
||||
void ignoreUser(BuildContext context) {
|
||||
if (controller.text.isEmpty) return;
|
||||
final userId = '@${controller.text}';
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.ignoreUser(userId),
|
||||
);
|
||||
controller.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsIgnoreListUI(this);
|
||||
}
|
||||
18
lib/pages/settings_multiple_emotes.dart
Normal file
18
lib/pages/settings_multiple_emotes.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'views/settings_multiple_emotes_ui.dart';
|
||||
|
||||
class MultipleEmotesSettings extends StatefulWidget {
|
||||
final String roomId;
|
||||
|
||||
MultipleEmotesSettings(this.roomId, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
MultipleEmotesSettingsController createState() =>
|
||||
MultipleEmotesSettingsController();
|
||||
}
|
||||
|
||||
class MultipleEmotesSettingsController extends State<MultipleEmotesSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) => MultipleEmotesSettingsUI(this);
|
||||
}
|
||||
112
lib/pages/settings_notifications.dart
Normal file
112
lib/pages/settings_notifications.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:open_noti_settings/open_noti_settings.dart';
|
||||
|
||||
import 'views/settings_notifications_ui.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class NotificationSettingsItem {
|
||||
final PushRuleKind type;
|
||||
final String key;
|
||||
final String Function(BuildContext) title;
|
||||
const NotificationSettingsItem(this.type, this.key, this.title);
|
||||
static List<NotificationSettingsItem> items = [
|
||||
NotificationSettingsItem(
|
||||
PushRuleKind.underride,
|
||||
'.m.rule.room_one_to_one',
|
||||
(c) => L10n.of(c).directChats,
|
||||
),
|
||||
NotificationSettingsItem(
|
||||
PushRuleKind.override,
|
||||
'.m.rule.contains_display_name',
|
||||
(c) => L10n.of(c).containsDisplayName,
|
||||
),
|
||||
NotificationSettingsItem(
|
||||
PushRuleKind.content,
|
||||
'.m.rule.contains_user_name',
|
||||
(c) => L10n.of(c).containsUserName,
|
||||
),
|
||||
NotificationSettingsItem(
|
||||
PushRuleKind.override,
|
||||
'.m.rule.invite_for_me',
|
||||
(c) => L10n.of(c).inviteForMe,
|
||||
),
|
||||
NotificationSettingsItem(
|
||||
PushRuleKind.override,
|
||||
'.m.rule.member_event',
|
||||
(c) => L10n.of(c).memberChanges,
|
||||
),
|
||||
NotificationSettingsItem(
|
||||
PushRuleKind.override,
|
||||
'.m.rule.suppress_notices',
|
||||
(c) => L10n.of(c).botMessages,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
class SettingsNotifications extends StatefulWidget {
|
||||
@override
|
||||
SettingsNotificationsController createState() =>
|
||||
SettingsNotificationsController();
|
||||
}
|
||||
|
||||
class SettingsNotificationsController extends State<SettingsNotifications> {
|
||||
void openAndroidNotificationSettingsAction() async {
|
||||
await NotificationSetting.configureChannel(
|
||||
NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
AppConfig.pushNotificationsChannelId,
|
||||
AppConfig.pushNotificationsChannelName,
|
||||
AppConfig.pushNotificationsChannelDescription,
|
||||
),
|
||||
),
|
||||
);
|
||||
return NotificationSetting.open();
|
||||
}
|
||||
|
||||
bool getNotificationSetting(NotificationSettingsItem item) {
|
||||
final pushRules = Matrix.of(context).client.globalPushRules;
|
||||
switch (item.type) {
|
||||
case PushRuleKind.content:
|
||||
return pushRules.content
|
||||
?.singleWhere((r) => r.ruleId == item.key, orElse: () => null)
|
||||
?.enabled;
|
||||
case PushRuleKind.override:
|
||||
return pushRules.override
|
||||
?.singleWhere((r) => r.ruleId == item.key, orElse: () => null)
|
||||
?.enabled;
|
||||
case PushRuleKind.room:
|
||||
return pushRules.room
|
||||
?.singleWhere((r) => r.ruleId == item.key, orElse: () => null)
|
||||
?.enabled;
|
||||
case PushRuleKind.sender:
|
||||
return pushRules.sender
|
||||
?.singleWhere((r) => r.ruleId == item.key, orElse: () => null)
|
||||
?.enabled;
|
||||
case PushRuleKind.underride:
|
||||
return pushRules.underride
|
||||
?.singleWhere((r) => r.ruleId == item.key, orElse: () => null)
|
||||
?.enabled;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setNotificationSetting(NotificationSettingsItem item, bool enabled) {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.setPushRuleEnabled(
|
||||
'global',
|
||||
item.type,
|
||||
item.key,
|
||||
enabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsNotificationsUI(this);
|
||||
}
|
||||
61
lib/pages/settings_style.dart
Normal file
61
lib/pages/settings_style.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'views/settings_style_ui.dart';
|
||||
import '../widgets/matrix.dart';
|
||||
|
||||
class SettingsStyle extends StatefulWidget {
|
||||
@override
|
||||
SettingsStyleController createState() => SettingsStyleController();
|
||||
}
|
||||
|
||||
class SettingsStyleController extends State<SettingsStyle> {
|
||||
void setWallpaperAction() 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(SettingKeys.wallpaper, wallpaper.path);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
void deleteWallpaperAction() async {
|
||||
Matrix.of(context).wallpaper = null;
|
||||
await Matrix.of(context).store.deleteItem(SettingKeys.wallpaper);
|
||||
setState(() => null);
|
||||
}
|
||||
|
||||
AdaptiveThemeMode currentTheme;
|
||||
|
||||
void switchTheme(AdaptiveThemeMode newTheme) {
|
||||
switch (newTheme) {
|
||||
case AdaptiveThemeMode.light:
|
||||
AdaptiveTheme.of(context).setLight();
|
||||
break;
|
||||
case AdaptiveThemeMode.dark:
|
||||
AdaptiveTheme.of(context).setDark();
|
||||
break;
|
||||
case AdaptiveThemeMode.system:
|
||||
AdaptiveTheme.of(context).setSystem();
|
||||
break;
|
||||
}
|
||||
setState(() => currentTheme = newTheme);
|
||||
}
|
||||
|
||||
void changeFontSizeFactor(double d) {
|
||||
setState(() => AppConfig.fontSizeFactor = d);
|
||||
Matrix.of(context).store.setItem(
|
||||
SettingKeys.fontSizeFactor,
|
||||
AppConfig.fontSizeFactor.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsStyleUI(this);
|
||||
}
|
||||
71
lib/pages/sign_up.dart
Normal file
71
lib/pages/sign_up.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
||||
import 'package:fluffychat/pages/views/sign_up_ui.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUp extends StatefulWidget {
|
||||
@override
|
||||
SignUpController createState() => SignUpController();
|
||||
}
|
||||
|
||||
class SignUpController extends State<SignUp> {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
String usernameError;
|
||||
bool loading = false;
|
||||
MatrixFile avatar;
|
||||
|
||||
void setAvatarAction() async {
|
||||
final file =
|
||||
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||
if (file != null) {
|
||||
setState(
|
||||
() => avatar = MatrixFile(
|
||||
bytes: file.toUint8List(),
|
||||
name: file.fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void resetAvatarAction() => setState(() => avatar = null);
|
||||
|
||||
void signUpAction([_]) async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (usernameController.text.isEmpty) {
|
||||
setState(() => usernameError = L10n.of(context).pleaseChooseAUsername);
|
||||
} else {
|
||||
setState(() => usernameError = null);
|
||||
}
|
||||
|
||||
if (usernameController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
setState(() => loading = true);
|
||||
|
||||
final preferredUsername =
|
||||
usernameController.text.toLowerCase().trim().replaceAll(' ', '-');
|
||||
|
||||
try {
|
||||
await matrix.client.checkUsernameAvailability(preferredUsername);
|
||||
} on MatrixException catch (exception) {
|
||||
setState(() => usernameError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
} catch (exception) {
|
||||
setState(() => usernameError = exception.toString());
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
setState(() => loading = false);
|
||||
await AdaptivePageLayout.of(context).pushNamed(
|
||||
'/signup/password/${Uri.encodeComponent(preferredUsername)}/${Uri.encodeComponent(usernameController.text)}',
|
||||
arguments: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SignUpUI(this);
|
||||
}
|
||||
115
lib/pages/sign_up_password.dart
Normal file
115
lib/pages/sign_up_password.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/get_client_secret.dart';
|
||||
import 'package:fluffychat/pages/views/sign_up_password_ui.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../utils/platform_infos.dart';
|
||||
|
||||
class SignUpPassword extends StatefulWidget {
|
||||
final MatrixFile avatar;
|
||||
final String username;
|
||||
final String displayname;
|
||||
const SignUpPassword(this.username, {this.avatar, this.displayname});
|
||||
@override
|
||||
SignUpPasswordController createState() => SignUpPasswordController();
|
||||
}
|
||||
|
||||
class SignUpPasswordController extends State<SignUpPassword> {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
String passwordError;
|
||||
String emailError;
|
||||
bool loading = false;
|
||||
bool showPassword = true;
|
||||
|
||||
void toggleShowPassword() => setState(() => showPassword = !showPassword);
|
||||
|
||||
void signUpAction() async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (passwordController.text.isEmpty) {
|
||||
setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword);
|
||||
} else {
|
||||
setState(() => passwordError = emailError = null);
|
||||
}
|
||||
|
||||
if (passwordController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() => loading = true);
|
||||
if (emailController.text.isNotEmpty) {
|
||||
emailController.text = emailController.text.trim();
|
||||
if (!EmailValidator.validate(emailController.text)) {
|
||||
setState(() => emailError = L10n.of(context).invalidEmail);
|
||||
return;
|
||||
}
|
||||
matrix.currentClientSecret = getClientSecret(30);
|
||||
Logs().d('Request email token');
|
||||
matrix.currentThreepidCreds = await matrix.client.requestEmailToken(
|
||||
emailController.text,
|
||||
matrix.currentClientSecret,
|
||||
1,
|
||||
);
|
||||
if (OkCancelResult.ok !=
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
message: L10n.of(context).weSentYouAnEmail,
|
||||
okLabel: L10n.of(context).confirm,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
)) {
|
||||
matrix.currentClientSecret = matrix.currentThreepidCreds = null;
|
||||
setState(() => loading = false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
final waitForLogin = matrix.client.onLoginStateChanged.stream.first;
|
||||
await matrix.client.uiaRequestBackground((auth) => matrix.client.register(
|
||||
username: widget.username,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
auth: auth,
|
||||
));
|
||||
if (matrix.currentClientSecret != null &&
|
||||
matrix.currentThreepidCreds != null) {
|
||||
Logs().d('Add third party identifier');
|
||||
await matrix.client.add3PID(
|
||||
matrix.currentClientSecret,
|
||||
matrix.currentThreepidCreds.sid,
|
||||
);
|
||||
}
|
||||
await waitForLogin;
|
||||
} catch (exception) {
|
||||
setState(() => emailError = exception.toString());
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
await matrix.client.onLoginStateChanged.stream
|
||||
.firstWhere((l) => l == LoginState.logged);
|
||||
// tchncs.de
|
||||
try {
|
||||
await matrix.client
|
||||
.setDisplayName(matrix.client.userID, widget.displayname);
|
||||
} catch (exception) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).couldNotSetDisplayname)));
|
||||
}
|
||||
if (widget.avatar != null) {
|
||||
try {
|
||||
await matrix.client.setAvatar(widget.avatar);
|
||||
} catch (exception) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).couldNotSetAvatar)));
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SignUpPasswordUI(this);
|
||||
}
|
||||
95
lib/pages/user_bottom_sheet.dart
Normal file
95
lib/pages/user_bottom_sheet.dart
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/permission_slider_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
|
||||
import 'views/user_bottom_sheet_ui.dart';
|
||||
|
||||
class UserBottomSheet extends StatefulWidget {
|
||||
final User user;
|
||||
final Function onMention;
|
||||
final BuildContext outerContext;
|
||||
|
||||
const UserBottomSheet({
|
||||
Key key,
|
||||
@required this.user,
|
||||
@required this.outerContext,
|
||||
this.onMention,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
UserBottomSheetController createState() => UserBottomSheetController();
|
||||
}
|
||||
|
||||
class UserBottomSheetController extends State<UserBottomSheet> {
|
||||
void participantAction(String action) async {
|
||||
final Function _askConfirmation =
|
||||
() async => (await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).no,
|
||||
) ==
|
||||
OkCancelResult.ok);
|
||||
switch (action) {
|
||||
case 'mention':
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
widget.onMention();
|
||||
break;
|
||||
case 'ban':
|
||||
if (await _askConfirmation()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.user.ban(),
|
||||
);
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
}
|
||||
break;
|
||||
case 'unban':
|
||||
if (await _askConfirmation()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.user.unban(),
|
||||
);
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
}
|
||||
break;
|
||||
case 'kick':
|
||||
if (await _askConfirmation()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.user.kick(),
|
||||
);
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
}
|
||||
break;
|
||||
case 'permission':
|
||||
final newPermission = await PermissionSliderDialog(
|
||||
initialPermission: widget.user.powerLevel)
|
||||
.show(context);
|
||||
if (newPermission != null) {
|
||||
if (newPermission == 100 && await _askConfirmation() == false) break;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.user.setPower(newPermission),
|
||||
);
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
}
|
||||
break;
|
||||
case 'message':
|
||||
final roomId = await widget.user.startDirectChat();
|
||||
await AdaptivePageLayout.of(widget.outerContext)
|
||||
.pushNamedAndRemoveUntilIsFirst('/rooms/$roomId');
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => UserBottomSheetUI(this);
|
||||
}
|
||||
59
lib/pages/views/archive_view.dart
Normal file
59
lib/pages/views/archive_view.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/archive.dart';
|
||||
import 'package:fluffychat/widgets/list_items/chat_list_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ArchiveView extends StatelessWidget {
|
||||
final ArchiveController controller;
|
||||
|
||||
const ArchiveView(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<Room>>(
|
||||
future: controller.getArchive(context),
|
||||
builder: (BuildContext context, snapshot) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).archive),
|
||||
actions: [
|
||||
if (snapshot.hasData &&
|
||||
controller.archive != null &&
|
||||
controller.archive.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: controller.forgetAllAction,
|
||||
child: Text(L10n.of(context).clearArchive),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
controller.archive = snapshot.data;
|
||||
if (controller.archive.isEmpty) {
|
||||
return Center(child: Icon(Icons.archive_outlined, size: 80));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
||||
controller.archive[i],
|
||||
onForget: controller.forgetAction,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
367
lib/pages/views/chat_details_ui.dart
Normal file
367
lib/pages/views/chat_details_ui.dart
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_details.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/content_banner.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/list_items/participant_list_item.dart';
|
||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
|
||||
import '../../utils/url_launcher.dart';
|
||||
|
||||
class ChatDetailsUI extends StatelessWidget {
|
||||
final ChatDetailsController controller;
|
||||
|
||||
const ChatDetailsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
if (room == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
controller.members.removeWhere((u) => u.membership == Membership.leave);
|
||||
final actualMembersCount =
|
||||
room.mInvitedMemberCount + room.mJoinedMemberCount;
|
||||
final canRequestMoreMembers =
|
||||
controller.members.length < actualMembersCount;
|
||||
return StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
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,
|
||||
actions: <Widget>[
|
||||
if (room.canonicalAlias?.isNotEmpty ?? false)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).share,
|
||||
icon: Icon(Icons.share_outlined),
|
||||
onPressed: () => FluffyShare.share(
|
||||
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
context),
|
||||
),
|
||||
ChatSettingsPopupMenu(room, false)
|
||||
],
|
||||
title: Text(
|
||||
room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.textTheme
|
||||
.headline6
|
||||
.color)),
|
||||
backgroundColor: Theme.of(context).appBarTheme.color,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: ContentBanner(room.avatar,
|
||||
onEdit: room.canSendEvent('m.room.avatar')
|
||||
? controller.setAvatarAction
|
||||
: null),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: MaxWidthBody(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.members.length +
|
||||
1 +
|
||||
(canRequestMoreMembers ? 1 : 0),
|
||||
itemBuilder: (BuildContext context, int i) => i == 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: room.canSendEvent('m.room.topic')
|
||||
? CircleAvatar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: Icon(Icons.edit_outlined),
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
'${L10n.of(context).groupDescription}:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold)),
|
||||
subtitle: LinkText(
|
||||
text: room.topic?.isEmpty ?? true
|
||||
? L10n.of(context).addGroupDescription
|
||||
: room.topic,
|
||||
linkStyle: TextStyle(color: Colors.blueAccent),
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText2
|
||||
.color,
|
||||
),
|
||||
onLinkTap: (url) =>
|
||||
UrlLauncher(context, url).launchUrl(),
|
||||
),
|
||||
onTap: room.canSendEvent('m.room.topic')
|
||||
? controller.setTopicAction
|
||||
: null,
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).settings,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (room.canSendEvent('m.room.name'))
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.people_outlined),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context).changeTheNameOfTheGroup),
|
||||
subtitle: Text(room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
onTap: controller.setDisplaynameAction,
|
||||
),
|
||||
if (room.joinRules == JoinRules.public)
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.link_outlined),
|
||||
),
|
||||
onTap: controller.editAliases,
|
||||
title: Text(L10n.of(context).editRoomAliases),
|
||||
subtitle: Text(
|
||||
(room.canonicalAlias?.isNotEmpty ?? false)
|
||||
? room.canonicalAlias
|
||||
: L10n.of(context).none),
|
||||
),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.insert_emoticon_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).emoteSettings),
|
||||
subtitle: Text(L10n.of(context).setCustomEmotes),
|
||||
onTap: controller.goToEmoteSettings,
|
||||
),
|
||||
PopupMenuButton(
|
||||
onSelected: controller.setJoinRulesAction,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<JoinRules>>[
|
||||
if (room.canChangeJoinRules)
|
||||
PopupMenuItem<JoinRules>(
|
||||
value: JoinRules.public,
|
||||
child: Text(JoinRules.public
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
),
|
||||
if (room.canChangeJoinRules)
|
||||
PopupMenuItem<JoinRules>(
|
||||
value: JoinRules.invite,
|
||||
child: Text(JoinRules.invite
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
),
|
||||
],
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.public_outlined)),
|
||||
title: Text(L10n.of(context)
|
||||
.whoIsAllowedToJoinThisGroup),
|
||||
subtitle: Text(
|
||||
room.joinRules.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
onSelected: controller.setHistoryVisibilityAction,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<HistoryVisibility>>[
|
||||
if (room.canChangeHistoryVisibility)
|
||||
PopupMenuItem<HistoryVisibility>(
|
||||
value: HistoryVisibility.invited,
|
||||
child: Text(HistoryVisibility.invited
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
),
|
||||
if (room.canChangeHistoryVisibility)
|
||||
PopupMenuItem<HistoryVisibility>(
|
||||
value: HistoryVisibility.joined,
|
||||
child: Text(HistoryVisibility.joined
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
),
|
||||
if (room.canChangeHistoryVisibility)
|
||||
PopupMenuItem<HistoryVisibility>(
|
||||
value: HistoryVisibility.shared,
|
||||
child: Text(HistoryVisibility.shared
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
),
|
||||
if (room.canChangeHistoryVisibility)
|
||||
PopupMenuItem<HistoryVisibility>(
|
||||
value: HistoryVisibility.worldReadable,
|
||||
child: Text(HistoryVisibility.worldReadable
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context)))),
|
||||
),
|
||||
],
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.visibility_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)
|
||||
.visibilityOfTheChatHistory),
|
||||
subtitle: Text(
|
||||
room.historyVisibility.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (room.joinRules == JoinRules.public)
|
||||
PopupMenuButton(
|
||||
onSelected: controller.setGuestAccessAction,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<GuestAccess>>[
|
||||
if (room.canChangeGuestAccess)
|
||||
PopupMenuItem<GuestAccess>(
|
||||
value: GuestAccess.canJoin,
|
||||
child: Text(
|
||||
GuestAccess.canJoin.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
),
|
||||
if (room.canChangeGuestAccess)
|
||||
PopupMenuItem<GuestAccess>(
|
||||
value: GuestAccess.forbidden,
|
||||
child: Text(
|
||||
GuestAccess.forbidden
|
||||
.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.info_outline),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context).areGuestsAllowedToJoin),
|
||||
subtitle: Text(
|
||||
room.guestAccess.getLocalizedString(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).editChatPermissions),
|
||||
subtitle: Text(
|
||||
L10n.of(context).whoCanPerformWhichAction),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.edit_attributes_outlined),
|
||||
),
|
||||
onTap: () => AdaptivePageLayout.of(context)
|
||||
.pushNamed('/rooms/${room.id}/permissions'),
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
actualMembersCount > 1
|
||||
? L10n.of(context).countParticipants(
|
||||
actualMembersCount.toString())
|
||||
: L10n.of(context).emptyChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
room.canInvite
|
||||
? ListTile(
|
||||
title: Text(L10n.of(context).inviteContact),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: Icon(Icons.add_outlined),
|
||||
),
|
||||
onTap: () => AdaptivePageLayout.of(context)
|
||||
.pushNamed('/rooms/${room.id}/invite'),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
)
|
||||
: i < controller.members.length + 1
|
||||
? ParticipantListItem(controller.members[i - 1])
|
||||
: ListTile(
|
||||
title: Text(L10n.of(context)
|
||||
.loadCountMoreParticipants(
|
||||
(actualMembersCount -
|
||||
controller.members.length)
|
||||
.toString())),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
onTap: controller.requestMoreMembersAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
199
lib/pages/views/chat_encryption_settings_ui.dart
Normal file
199
lib/pages/views/chat_encryption_settings_ui.dart
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/chat_encryption_settings.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../utils/device_extension.dart';
|
||||
|
||||
class ChatEncryptionSettingsUI extends StatelessWidget {
|
||||
final ChatEncryptionSettingsController controller;
|
||||
|
||||
const ChatEncryptionSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(controller.widget.id);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).tapOnDeviceToVerify),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(56),
|
||||
child: ListTile(
|
||||
title: Text(L10n.of(context).deviceVerifyDescription),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
child: Icon(Icons.lock),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(L10n.of(context).oopsSomethingWentWrong +
|
||||
': ' +
|
||||
snapshot.error.toString()),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final deviceKeys = snapshot.data;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (i == 0 ||
|
||||
deviceKeys[i].userId !=
|
||||
deviceKeys[i - 1].userId) ...{
|
||||
Divider(height: 1, thickness: 1),
|
||||
PopupMenuButton(
|
||||
onSelected: (action) => controller.onSelected(
|
||||
context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (room
|
||||
.client
|
||||
.userDeviceKeys[deviceKeys[i].userId]
|
||||
.verified ==
|
||||
UserVerifiedStatus.unknown) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'verify_user',
|
||||
child: Text(L10n.of(context).verifyUser),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: Avatar(
|
||||
room
|
||||
.getUserByMXIDSync(deviceKeys[i].userId)
|
||||
.avatarUrl,
|
||||
room
|
||||
.getUserByMXIDSync(deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
room
|
||||
.getUserByMXIDSync(deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
deviceKeys[i].userId,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.color
|
||||
.withAlpha(150),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
PopupMenuButton(
|
||||
onSelected: (action) => controller.onSelected(
|
||||
context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (deviceKeys[i].blocked ||
|
||||
!deviceKeys[i].verified) {
|
||||
items.add(PopupMenuItem(
|
||||
value:
|
||||
deviceKeys[i].userId == room.client.userID
|
||||
? 'verify'
|
||||
: 'verify_user',
|
||||
child: Text(L10n.of(context).verifyStart),
|
||||
));
|
||||
}
|
||||
if (deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'unblock',
|
||||
child: Text(L10n.of(context).unblockDevice),
|
||||
));
|
||||
}
|
||||
if (!deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'block',
|
||||
child: Text(L10n.of(context).blockDevice),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyText1.color,
|
||||
backgroundColor:
|
||||
Theme.of(context).secondaryHeaderColor,
|
||||
child: Icon(deviceKeys[i].icon),
|
||||
),
|
||||
title: Text(
|
||||
deviceKeys[i].displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
deviceKeys[i].deviceId,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.color
|
||||
.withAlpha(150),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
deviceKeys[i].blocked
|
||||
? L10n.of(context).blocked
|
||||
: deviceKeys[i].verified
|
||||
? L10n.of(context).verified
|
||||
: L10n.of(context).unknownDevice,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: deviceKeys[i].blocked
|
||||
? Colors.red
|
||||
: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
242
lib/pages/views/chat_list_ui.dart
Normal file
242
lib/pages/views/chat_list_ui.dart
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/chat_list.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/widgets/list_items/chat_list_item.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ChatListUI extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const ChatListUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<Object>(
|
||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
||||
builder: (_, __) {
|
||||
final selectMode = Matrix.of(context).shareContent != null
|
||||
? SelectMode.share
|
||||
: controller.selectedRoomIds.isEmpty
|
||||
? SelectMode.normal
|
||||
: SelectMode.select;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation:
|
||||
AdaptivePageLayout.of(context).columnMode(context) ? 1 : null,
|
||||
leading: selectMode == SelectMode.normal
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: selectMode == SelectMode.share
|
||||
? null
|
||||
: selectMode == SelectMode.select
|
||||
? [
|
||||
if (controller.selectedRoomIds.length == 1)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).toggleUnread,
|
||||
icon: Icon(Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(
|
||||
controller.selectedRoomIds.single)
|
||||
.isUnread
|
||||
? Icons.mark_chat_read_outlined
|
||||
: Icons.mark_chat_unread_outlined),
|
||||
onPressed: controller.toggleUnread,
|
||||
),
|
||||
if (controller.selectedRoomIds.length == 1)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).toggleFavorite,
|
||||
icon: Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.toggleFavouriteRoom,
|
||||
),
|
||||
if (controller.selectedRoomIds.length == 1)
|
||||
IconButton(
|
||||
icon: Icon(Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(
|
||||
controller.selectedRoomIds.single)
|
||||
.pushRuleState ==
|
||||
PushRuleState.notify
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_outlined),
|
||||
tooltip: L10n.of(context).toggleMuted,
|
||||
onPressed: controller.toggleMuted,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.archive_outlined),
|
||||
tooltip: L10n.of(context).archive,
|
||||
onPressed: controller.archiveAction,
|
||||
),
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.search_outlined),
|
||||
tooltip: L10n.of(context).search,
|
||||
onPressed: () => AdaptivePageLayout.of(context)
|
||||
.pushNamed('/search'),
|
||||
),
|
||||
PopupMenuButton<PopupMenuAction>(
|
||||
onSelected: controller.onPopupMenuSelect,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.setStatus,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.edit_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).setStatus),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.newGroup,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.group_add_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).createNewGroup),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.share_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).inviteContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.archive,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.archive_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(L10n.of(context).archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
controller.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: controller.waitForFirstSync(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final rooms = List<Room>.from(
|
||||
Matrix.of(context).client.rooms);
|
||||
rooms.removeWhere((room) => room.lastEvent == null);
|
||||
if (rooms.isEmpty) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.maps_ugc_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
L10n.of(context).startYourFirstChat,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final totalCount = rooms.length;
|
||||
return ListView.builder(
|
||||
itemCount: totalCount,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
ChatListItem(
|
||||
rooms[i],
|
||||
selected: controller.selectedRoomIds
|
||||
.contains(rooms[i].id),
|
||||
onTap: selectMode == SelectMode.select
|
||||
? () =>
|
||||
controller.toggleSelection(rooms[i].id)
|
||||
: null,
|
||||
onLongPress: () =>
|
||||
controller.toggleSelection(rooms[i].id),
|
||||
activeChat:
|
||||
controller.widget.activeChat == rooms[i].id,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
floatingActionButton: selectMode == SelectMode.normal
|
||||
? FloatingActionButton(
|
||||
onPressed: () => AdaptivePageLayout.of(context)
|
||||
.pushNamedAndRemoveUntilIsFirst('/newprivatechat'),
|
||||
child: Icon(CupertinoIcons.chat_bubble),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListPopupMenuItemActions {
|
||||
createGroup,
|
||||
discover,
|
||||
setStatus,
|
||||
inviteContact,
|
||||
settings,
|
||||
}
|
||||
124
lib/pages/views/chat_permissions_settings_ui.dart
Normal file
124
lib/pages/views/chat_permissions_settings_ui.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'package:fluffychat/pages/chat_permissions_settings.dart';
|
||||
import 'package:fluffychat/widgets/list_items/permission_list_tile.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class ChatPermissionsSettingsUI extends StatelessWidget {
|
||||
final ChatPermissionsSettingsController controller;
|
||||
|
||||
const ChatPermissionsSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).editChatPermissions),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: controller.onChanged,
|
||||
builder: (context, _) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
final powerLevelsContent = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels).content);
|
||||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||
..removeWhere((k, v) => !(v is int));
|
||||
final eventsPowerLevels =
|
||||
Map<String, dynamic>.from(powerLevelsContent['events'] ?? {})
|
||||
..removeWhere((k, v) => !(v is int));
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var entry in powerLevels.entries)
|
||||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
permission: entry.value,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, entry.key, entry.value),
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).notifications,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final key = 'rooms';
|
||||
final int value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent['notifications']['rooms'] ?? 0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
permission: value,
|
||||
category: 'notifications',
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, key, value,
|
||||
category: 'notifications'),
|
||||
);
|
||||
}),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).configureChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (eventsPowerLevels != null)
|
||||
for (var entry in eventsPowerLevels.entries)
|
||||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
category: 'events',
|
||||
permission: entry.value,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, entry.key, entry.value,
|
||||
category: 'events'),
|
||||
),
|
||||
if (room.canSendEvent(EventTypes.RoomTombstone)) ...{
|
||||
Divider(thickness: 1),
|
||||
FutureBuilder<ServerCapabilities>(
|
||||
future: room.client.getCapabilities(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final String roomVersion = room
|
||||
.getState(EventTypes.RoomCreate)
|
||||
.content['room_version'] ??
|
||||
'1';
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${L10n.of(context).roomVersion}: $roomVersion'),
|
||||
onTap: () =>
|
||||
controller.updateRoomAction(snapshot.data),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
756
lib/pages/views/chat_ui.dart
Normal file
756
lib/pages/views/chat_ui.dart
Normal file
|
|
@ -0,0 +1,756 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/chat.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/widgets/input_bar.dart';
|
||||
import 'package:fluffychat/widgets/unread_badge_back_button.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/encryption_button.dart';
|
||||
import 'package:fluffychat/widgets/list_items/message.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/reply_content.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/config/app_emojis.dart';
|
||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/utils/room_status_extension.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:swipe_to_action/swipe_to_action.dart';
|
||||
|
||||
class ChatUI extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const ChatUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.matrix = Matrix.of(context);
|
||||
final client = controller.matrix.client;
|
||||
controller.room ??= client.getRoomById(controller.widget.id);
|
||||
if (controller.room == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
);
|
||||
}
|
||||
controller.matrix.client.activeRoomId = controller.widget.id;
|
||||
|
||||
if (controller.room.membership == Membership.invite) {
|
||||
showFutureLoadingDialog(
|
||||
context: context, future: () => controller.room.join());
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context).close,
|
||||
)
|
||||
: AdaptivePageLayout.of(context).columnMode(context)
|
||||
? null
|
||||
: UnreadBadgeBackButton(roomId: controller.widget.id),
|
||||
titleSpacing:
|
||||
AdaptivePageLayout.of(context).columnMode(context) ? null : 0,
|
||||
title: controller.selectedEvents.isEmpty
|
||||
? StreamBuilder(
|
||||
stream: controller.room.onUpdate.stream,
|
||||
builder: (context, snapshot) => ListTile(
|
||||
leading: Avatar(
|
||||
controller.room.avatar, controller.room.displayname),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: controller.room.isDirectChat
|
||||
? () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: controller.room.getUserByMXIDSync(
|
||||
controller.room.directChatMatrixID),
|
||||
outerContext: context,
|
||||
onMention: () => controller
|
||||
.sendController.text +=
|
||||
'${controller.room.directChatMatrixID} ',
|
||||
),
|
||||
)
|
||||
: () => (!AdaptivePageLayout.of(context)
|
||||
.columnMode(context) ||
|
||||
AdaptivePageLayout.of(context)
|
||||
.viewDataStack
|
||||
.length <
|
||||
3)
|
||||
? AdaptivePageLayout.of(context).pushNamed(
|
||||
'/rooms/${controller.room.id}/details')
|
||||
: null,
|
||||
title: Text(
|
||||
controller.room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context))),
|
||||
maxLines: 1),
|
||||
subtitle: controller.room
|
||||
.getLocalizedTypingText(context)
|
||||
.isEmpty
|
||||
? StreamBuilder<Object>(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onPresence
|
||||
.stream
|
||||
.where((p) =>
|
||||
p.senderId ==
|
||||
controller.room.directChatMatrixID),
|
||||
builder: (context, snapshot) => Text(
|
||||
controller.room.getLocalizedStatus(context),
|
||||
maxLines: 1,
|
||||
//overflow: TextOverflow.ellipsis,
|
||||
))
|
||||
: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.edit_outlined,
|
||||
color: Theme.of(context).accentColor,
|
||||
size: 13),
|
||||
SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.room
|
||||
.getLocalizedTypingText(context),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
: Text(L10n.of(context)
|
||||
.numberSelected(controller.selectedEvents.length.toString())),
|
||||
actions: controller.selectMode
|
||||
? <Widget>[
|
||||
if (controller.selectedEvents.length == 1 &&
|
||||
controller.selectedEvents.first.status > 0 &&
|
||||
controller.selectedEvents.first.senderId == client.userID)
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit_outlined),
|
||||
tooltip: L10n.of(context).edit,
|
||||
onPressed: controller.editSelectedEventAction,
|
||||
),
|
||||
PopupMenuButton(
|
||||
onSelected: controller.onEventActionPopupMenuSelected,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: 'copy',
|
||||
child: Text(L10n.of(context).copy),
|
||||
),
|
||||
if (controller.canRedactSelectedEvents)
|
||||
PopupMenuItem(
|
||||
value: 'redact',
|
||||
child: Text(
|
||||
L10n.of(context).redactMessage,
|
||||
style: TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
if (controller.selectedEvents.length == 1)
|
||||
PopupMenuItem(
|
||||
value: 'report',
|
||||
child: Text(
|
||||
L10n.of(context).reportMessage,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
if (controller.room.canSendDefaultStates)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).videoCall,
|
||||
icon: Icon(Icons.video_call_outlined),
|
||||
onPressed: controller.startCallAction,
|
||||
),
|
||||
ChatSettingsPopupMenu(
|
||||
controller.room, !controller.room.isDirectChat),
|
||||
],
|
||||
),
|
||||
floatingActionButton: controller.showScrollDownButton
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyText2.color,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
mini: true,
|
||||
child: Icon(Icons.arrow_downward_outlined,
|
||||
color: Theme.of(context).primaryColor),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
if (Matrix.of(context).wallpaper != null)
|
||||
Image.file(
|
||||
Matrix.of(context).wallpaper,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ConnectionStatusHeader(),
|
||||
if (controller.room.getState(EventTypes.RoomTombstone) != null)
|
||||
Container(
|
||||
height: 72,
|
||||
child: Material(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
child: Icon(Icons.upgrade_outlined),
|
||||
),
|
||||
title: Text(
|
||||
controller.room
|
||||
.getState(EventTypes.RoomTombstone)
|
||||
.parsedTombstoneContent
|
||||
.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(L10n.of(context).goToTheNewRoom),
|
||||
onTap: controller.goToNewRoomAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder<bool>(
|
||||
future: controller.getTimeline(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (controller.timeline == null) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
// create a map of eventId --> index to greatly improve performance of
|
||||
// ListView's findChildIndexCallback
|
||||
final thisEventsKeyMap = <String, int>{};
|
||||
for (var i = 0;
|
||||
i < controller.filteredEvents.length;
|
||||
i++) {
|
||||
thisEventsKeyMap[controller.filteredEvents[i].eventId] =
|
||||
i;
|
||||
}
|
||||
|
||||
final horizontalPadding = max(
|
||||
0,
|
||||
(MediaQuery.of(context).size.width -
|
||||
FluffyThemes.columnWidth *
|
||||
(AdaptivePageLayout.of(context)
|
||||
.currentViewData
|
||||
?.rightView !=
|
||||
null
|
||||
? 4.5
|
||||
: 3.5)) /
|
||||
2)
|
||||
.toDouble();
|
||||
|
||||
return ListView.custom(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom: 4,
|
||||
left: horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
),
|
||||
reverse: true,
|
||||
controller: controller.scrollController,
|
||||
childrenDelegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int i) {
|
||||
return i == controller.filteredEvents.length + 1
|
||||
? controller.timeline.isRequestingHistory
|
||||
? Container(
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: controller.canLoadMore
|
||||
? TextButton(
|
||||
onPressed:
|
||||
controller.requestHistory,
|
||||
child: Text(
|
||||
L10n.of(context).loadMore,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container()
|
||||
: i == 0
|
||||
? StreamBuilder(
|
||||
stream: controller.room.onUpdate.stream,
|
||||
builder: (_, __) {
|
||||
final seenByText = controller.room
|
||||
.getLocalizedSeenByText(
|
||||
context,
|
||||
controller.timeline,
|
||||
controller.filteredEvents,
|
||||
controller.unfolded,
|
||||
);
|
||||
return AnimatedContainer(
|
||||
height: seenByText.isEmpty ? 0 : 24,
|
||||
duration: seenByText.isEmpty
|
||||
? Duration(milliseconds: 0)
|
||||
: Duration(milliseconds: 300),
|
||||
alignment: controller.filteredEvents
|
||||
.first.senderId ==
|
||||
client.userID
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.8),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
seenByText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.accentColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: AutoScrollTag(
|
||||
key: ValueKey(controller
|
||||
.filteredEvents[i - 1].eventId),
|
||||
index: i - 1,
|
||||
controller: controller.scrollController,
|
||||
child: Swipeable(
|
||||
key: ValueKey(controller
|
||||
.filteredEvents[i - 1].eventId),
|
||||
background: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.0),
|
||||
child: Center(
|
||||
child: Icon(Icons.reply_outlined),
|
||||
),
|
||||
),
|
||||
direction: SwipeDirection.endToStart,
|
||||
onSwipe: (direction) =>
|
||||
controller.replyAction(
|
||||
replyTo: controller
|
||||
.filteredEvents[i - 1]),
|
||||
child: Message(
|
||||
controller.filteredEvents[i - 1],
|
||||
onAvatarTab: (Event event) =>
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) =>
|
||||
UserBottomSheet(
|
||||
user: event.sender,
|
||||
outerContext: context,
|
||||
onMention: () => controller
|
||||
.sendController
|
||||
.text +=
|
||||
'${event.senderId} ',
|
||||
),
|
||||
),
|
||||
unfold: controller.unfold,
|
||||
onSelect:
|
||||
controller.onSelectMessage,
|
||||
scrollToEventId:
|
||||
(String eventId) => controller
|
||||
.scrollToEventId(eventId),
|
||||
longPressSelect: controller
|
||||
.selectedEvents.isEmpty,
|
||||
selected: controller
|
||||
.selectedEvents
|
||||
.contains(controller
|
||||
.filteredEvents[i - 1]),
|
||||
timeline: controller.timeline,
|
||||
nextEvent: i >= 2
|
||||
? controller
|
||||
.filteredEvents[i - 2]
|
||||
: null),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: controller.filteredEvents.length + 2,
|
||||
findChildIndexCallback: (key) => controller
|
||||
.findChildIndexCallback(key, thisEventsKeyMap),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!controller.showEmojiPicker)
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: (controller.editEvent == null &&
|
||||
controller.replyEvent == null &&
|
||||
controller.room.canSendDefaultMessages &&
|
||||
controller.selectedEvents.length == 1)
|
||||
? 56
|
||||
: 0,
|
||||
child: Material(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
child: Builder(builder: (context) {
|
||||
if (!(controller.editEvent == null &&
|
||||
controller.replyEvent == null &&
|
||||
controller.selectedEvents.length == 1)) {
|
||||
return Container();
|
||||
}
|
||||
final emojis = List<String>.from(AppEmojis.emojis);
|
||||
final allReactionEvents = controller
|
||||
.selectedEvents.first
|
||||
.aggregatedEvents(
|
||||
controller.timeline, RelationshipTypes.reaction)
|
||||
?.where((event) =>
|
||||
event.senderId == event.room.client.userID &&
|
||||
event.type == 'm.reaction');
|
||||
|
||||
allReactionEvents.forEach((event) {
|
||||
try {
|
||||
emojis.remove(event.content['m.relates_to']['key']);
|
||||
} catch (_) {}
|
||||
});
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: emojis.length + 1,
|
||||
itemBuilder: (c, i) => i == emojis.length
|
||||
? InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => controller
|
||||
.pickEmojiAction(allReactionEvents),
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.add_outlined),
|
||||
),
|
||||
)
|
||||
: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () =>
|
||||
controller.sendEmojiAction(emojis[i]),
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
emojis[i],
|
||||
style: TextStyle(fontSize: 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: controller.editEvent != null ||
|
||||
controller.replyEvent != null
|
||||
? 56
|
||||
: 0,
|
||||
child: Material(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
tooltip: L10n.of(context).close,
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: controller.cancelReplyEventAction,
|
||||
),
|
||||
Expanded(
|
||||
child: controller.replyEvent != null
|
||||
? ReplyContent(controller.replyEvent,
|
||||
timeline: controller.timeline)
|
||||
: _EditContent(controller.editEvent
|
||||
?.getDisplayEvent(controller.timeline)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join &&
|
||||
!controller.showEmojiPicker)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: controller.selectMode
|
||||
? <Widget>[
|
||||
Container(
|
||||
height: 56,
|
||||
child: TextButton(
|
||||
onPressed: controller.forwardEventsAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.keyboard_arrow_left_outlined),
|
||||
Text(L10n.of(context).forward),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller.selectedEvents.length == 1
|
||||
? controller.selectedEvents.first
|
||||
.getDisplayEvent(
|
||||
controller.timeline)
|
||||
.status >
|
||||
0
|
||||
? Container(
|
||||
height: 56,
|
||||
child: TextButton(
|
||||
onPressed: controller.replyAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context).reply),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 56,
|
||||
child: TextButton(
|
||||
onPressed:
|
||||
controller.sendAgainAction,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context)
|
||||
.tryToSendAgain),
|
||||
SizedBox(width: 4),
|
||||
Icon(Icons.send_outlined,
|
||||
size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
]
|
||||
: <Widget>[
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 200),
|
||||
height: 56,
|
||||
width: controller.inputText.isEmpty ? 56 : 0,
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: Icon(Icons.add_outlined),
|
||||
onSelected:
|
||||
controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'file',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
child:
|
||||
Icon(Icons.attachment_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendFile),
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'image',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.image_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendImage),
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.purple,
|
||||
foregroundColor: Colors.white,
|
||||
child:
|
||||
Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
title:
|
||||
Text(L10n.of(context).openCamera),
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'voice',
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
child:
|
||||
Icon(Icons.mic_none_outlined),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context).voiceMessage),
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: EncryptionButton(controller.room),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: InputBar(
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: kIsWeb ? 1 : 8,
|
||||
autofocus: !PlatformInfos.isMobile,
|
||||
keyboardType: !PlatformInfos.isMobile
|
||||
? TextInputType.text
|
||||
: TextInputType.multiline,
|
||||
onSubmitted: controller.onInputBarSubmitted,
|
||||
focusNode: controller.inputFocus,
|
||||
controller: controller.sendController,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).writeAMessage,
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
filled: false,
|
||||
),
|
||||
onChanged: controller.onInputBarChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.inputText.isEmpty)
|
||||
Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context).voiceMessage,
|
||||
icon: Icon(Icons.mic_none_outlined),
|
||||
onPressed: controller.voiceMessageAction,
|
||||
),
|
||||
),
|
||||
if (!PlatformInfos.isMobile ||
|
||||
controller.inputText.isNotEmpty)
|
||||
Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.send_outlined),
|
||||
onPressed: controller.send,
|
||||
tooltip: L10n.of(context).send,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: controller.showEmojiPicker
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: 0,
|
||||
child: controller.showEmojiPicker
|
||||
? EmojiPicker(
|
||||
onEmojiSelected: controller.onEmojiSelected,
|
||||
onBackspacePressed: controller.cancelEmojiPicker,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EditContent extends StatelessWidget {
|
||||
final Event event;
|
||||
|
||||
_EditContent(this.event);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (event == null) {
|
||||
return Container();
|
||||
}
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
Container(width: 15.0),
|
||||
Text(
|
||||
event?.getLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
) ??
|
||||
'',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyText2.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/pages/views/device_settings_ui.dart
Normal file
97
lib/pages/views/device_settings_ui.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:fluffychat/pages/device_settings.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../widgets/list_items/user_device_list_item.dart';
|
||||
|
||||
class DevicesSettingsUI extends StatelessWidget {
|
||||
final DevicesSettingsController controller;
|
||||
|
||||
const DevicesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).devices),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: FutureBuilder<bool>(
|
||||
future: controller.loadUserDevices(context),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.error_outlined),
|
||||
Text(snapshot.error.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData || controller.devices == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (controller.thisDevice != null)
|
||||
UserDeviceListItem(
|
||||
controller.thisDevice,
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
Divider(height: 1),
|
||||
if (controller.notThisDevice.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
controller.errorDeletingDevices ??
|
||||
L10n.of(context).removeAllOtherDevices,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
trailing: controller.loadingDeletingDevices
|
||||
? CircularProgressIndicator()
|
||||
: Icon(Icons.delete_outline),
|
||||
onTap: controller.loadingDeletingDevices
|
||||
? null
|
||||
: () => controller
|
||||
.removeDevicesAction(controller.notThisDevice),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: controller.notThisDevice.isEmpty
|
||||
? Center(
|
||||
child: Icon(
|
||||
Icons.devices_other,
|
||||
size: 60,
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) =>
|
||||
Divider(height: 1),
|
||||
itemCount: controller.notThisDevice.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
UserDeviceListItem(
|
||||
controller.notThisDevice[i],
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/pages/views/empty_page_ui.dart
Normal file
12
lib/pages/views/empty_page_ui.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmptyPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Image.asset('assets/favicon.png', width: 100, height: 100),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/pages/views/homeserver_picker_ui.dart
Normal file
109
lib/pages/views/homeserver_picker_ui.dart
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import '../homeserver_picker.dart';
|
||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
|
||||
import 'package:fluffychat/widgets/fluffy_banner.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class HomeserverPickerUI extends StatelessWidget {
|
||||
final HomeserverPickerController controller;
|
||||
|
||||
const HomeserverPickerUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 8,
|
||||
title: DefaultAppBarSearchField(
|
||||
prefixText: 'https://',
|
||||
hintText: L10n.of(context).enterYourHomeserver,
|
||||
searchController: controller.homeserverController,
|
||||
suffix: Icon(Icons.edit_outlined),
|
||||
padding: EdgeInsets.zero,
|
||||
onChanged: (s) => controller.domain = s,
|
||||
readOnly: !AppConfig.allowOtherHomeservers,
|
||||
onSubmit: (_) => controller.checkHomeserverAction(),
|
||||
unfocusOnClear: false,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'loginBanner',
|
||||
child: FluffyBanner(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
AppConfig.applicationWelcomeMessage ??
|
||||
L10n.of(context).welcomeText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: controller.checkHomeserverAction,
|
||||
child: controller.isLoading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).connect.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => launch(AppConfig.privacyUrl),
|
||||
child: Text(
|
||||
L10n.of(context).privacy,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => PlatformInfos.showDialog(context),
|
||||
child: Text(
|
||||
L10n.of(context).about,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/pages/views/image_viewer_ui.dart
Normal file
59
lib/pages/views/image_viewer_ui.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import '../image_viewer.dart';
|
||||
import 'package:fluffychat/widgets/image_bubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ImageViewerUI extends StatelessWidget {
|
||||
final ImageViewerController controller;
|
||||
|
||||
const ImageViewerUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
color: Colors.white,
|
||||
tooltip: L10n.of(context).close,
|
||||
),
|
||||
backgroundColor: Color(0x44000000),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.reply_outlined),
|
||||
onPressed: controller.forwardAction,
|
||||
color: Colors.white,
|
||||
tooltip: L10n.of(context).share,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.download_outlined),
|
||||
onPressed: controller.openFileAction,
|
||||
color: Colors.white,
|
||||
tooltip: L10n.of(context).downloadFile,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: InteractiveViewer(
|
||||
minScale: 1.0,
|
||||
maxScale: 10.0,
|
||||
onInteractionEnd: controller.onInteractionEnds,
|
||||
child: Center(
|
||||
child: ImageBubble(
|
||||
controller.widget.event,
|
||||
tapToView: false,
|
||||
onLoaded: controller.widget.onLoaded,
|
||||
fit: BoxFit.contain,
|
||||
backgroundColor: Colors.black,
|
||||
maxSize: false,
|
||||
radius: 0.0,
|
||||
thumbnailOnly: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
83
lib/pages/views/invitation_selection_ui.dart
Normal file
83
lib/pages/views/invitation_selection_ui.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:fluffychat/pages/invitation_selection.dart';
|
||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class InvitationSelectionUI extends StatelessWidget {
|
||||
final InvitationSelectionController controller;
|
||||
|
||||
const InvitationSelectionUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
final groupName =
|
||||
room.name?.isEmpty ?? false ? L10n.of(context).group : room.name;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
titleSpacing: 0,
|
||||
title: DefaultAppBarSearchField(
|
||||
autofocus: true,
|
||||
hintText: L10n.of(context).inviteContactToGroup(groupName),
|
||||
onChanged: controller.searchUserWithCoolDown,
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: Avatar(
|
||||
controller.foundProfiles[i].avatarUrl,
|
||||
controller.foundProfiles[i].displayname ??
|
||||
controller.foundProfiles[i].userId,
|
||||
),
|
||||
title: Text(
|
||||
controller.foundProfiles[i].displayname ??
|
||||
controller.foundProfiles[i].userId.localpart,
|
||||
),
|
||||
subtitle: Text(controller.foundProfiles[i].userId),
|
||||
onTap: () => controller.inviteAction(
|
||||
context, controller.foundProfiles[i].userId),
|
||||
),
|
||||
)
|
||||
: FutureBuilder<List<User>>(
|
||||
future: controller.getContacts(context),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
final contacts = snapshot.data;
|
||||
return ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: contacts.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: Avatar(
|
||||
contacts[i].avatarUrl,
|
||||
contacts[i].calcDisplayname(),
|
||||
),
|
||||
title: Text(contacts[i].calcDisplayname()),
|
||||
subtitle: Text(contacts[i].id),
|
||||
onTap: () =>
|
||||
controller.inviteAction(context, contacts[i].id),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
285
lib/pages/views/login_ui.dart
Normal file
285
lib/pages/views/login_ui.dart
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
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/widgets/layouts/one_page_card.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../utils/platform_infos.dart';
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
|
||||
class Login extends StatefulWidget {
|
||||
@override
|
||||
_LoginState createState() => _LoginState();
|
||||
}
|
||||
|
||||
class _LoginState extends State<Login> {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
String usernameError;
|
||||
String passwordError;
|
||||
bool loading = false;
|
||||
bool showPassword = false;
|
||||
|
||||
void login(BuildContext context) async {
|
||||
final matrix = Matrix.of(context);
|
||||
if (usernameController.text.isEmpty) {
|
||||
setState(() => usernameError = L10n.of(context).pleaseEnterYourUsername);
|
||||
} else {
|
||||
setState(() => usernameError = null);
|
||||
}
|
||||
if (passwordController.text.isEmpty) {
|
||||
setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword);
|
||||
} else {
|
||||
setState(() => passwordError = null);
|
||||
}
|
||||
|
||||
if (usernameController.text.isEmpty || passwordController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
final username = usernameController.text;
|
||||
AuthenticationIdentifier identifier;
|
||||
if (username.isEmail) {
|
||||
identifier = AuthenticationThirdPartyIdentifier(
|
||||
medium: 'email',
|
||||
address: username,
|
||||
);
|
||||
} else if (username.isPhoneNumber) {
|
||||
identifier = AuthenticationThirdPartyIdentifier(
|
||||
medium: 'msisdn',
|
||||
address: username,
|
||||
);
|
||||
} else {
|
||||
identifier = AuthenticationUserIdentifier(user: username);
|
||||
}
|
||||
await matrix.client.login(
|
||||
identifier: identifier,
|
||||
// To stay compatible with older server versions
|
||||
// ignore: deprecated_member_use
|
||||
user: identifier.type == AuthenticationIdentifierTypes.userId
|
||||
? username
|
||||
: null,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName);
|
||||
} on MatrixException catch (exception) {
|
||||
setState(() => passwordError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
} catch (exception) {
|
||||
setState(() => passwordError = exception.toString());
|
||||
return setState(() => loading = false);
|
||||
}
|
||||
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
|
||||
Timer _coolDown;
|
||||
|
||||
void _checkWellKnownWithCoolDown(String userId, BuildContext context) async {
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(
|
||||
Duration(seconds: 1),
|
||||
() => _checkWellKnown(userId, context),
|
||||
);
|
||||
}
|
||||
|
||||
void _checkWellKnown(String userId, BuildContext context) async {
|
||||
setState(() => usernameError = null);
|
||||
if (!userId.isValidMatrixId) return;
|
||||
try {
|
||||
final wellKnownInformations = await Matrix.of(context)
|
||||
.client
|
||||
.getWellKnownInformationsByUserId(userId);
|
||||
final newDomain = wellKnownInformations.mHomeserver?.baseUrl;
|
||||
if ((newDomain?.isNotEmpty ?? false) &&
|
||||
newDomain != Matrix.of(context).client.homeserver.toString()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.checkHomeserver(newDomain),
|
||||
);
|
||||
setState(() => usernameError = null);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => usernameError = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _passwordForgotten(BuildContext context) async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).enterAnEmailAddress,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context).enterAnEmailAddress,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final response = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.resetPasswordUsingEmail(
|
||||
input.single,
|
||||
clientSecret,
|
||||
sendAttempt++,
|
||||
),
|
||||
);
|
||||
if (response.error != null) return;
|
||||
final ok = await showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).weSentYouAnEmail,
|
||||
message: L10n.of(context).pleaseClickOnLink,
|
||||
okLabel: L10n.of(context).iHaveClickedOnLink,
|
||||
useRootNavigator: false,
|
||||
);
|
||||
if (ok == null) return;
|
||||
final password = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).chooseAStrongPassword,
|
||||
okLabel: L10n.of(context).ok,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
useRootNavigator: false,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: '******',
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (password == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.changePassword(
|
||||
password.single,
|
||||
auth: AuthenticationThreePidCreds(
|
||||
type: AuthenticationTypes.emailIdentity,
|
||||
threepidCreds: [
|
||||
ThreepidCreds(
|
||||
sid: (response as RequestTokenResponse).sid,
|
||||
clientSecret: clientSecret,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (success.error == null) {
|
||||
AdaptivePageLayout.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)));
|
||||
}
|
||||
}
|
||||
|
||||
static int sendAttempt = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: loading ? Container() : BackButton(),
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
L10n.of(context).logInTo(Matrix.of(context)
|
||||
.client
|
||||
.homeserver
|
||||
.toString()
|
||||
.replaceFirst('https://', '')),
|
||||
),
|
||||
),
|
||||
body: Builder(builder: (context) {
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
readOnly: loading,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
onChanged: (t) => _checkWellKnownWithCoolDown(t, context),
|
||||
controller: usernameController,
|
||||
autofillHints: loading ? null : [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.account_box_outlined),
|
||||
hintText:
|
||||
'@${L10n.of(context).username.toLowerCase()}:domain',
|
||||
errorText: usernameError,
|
||||
labelText: L10n.of(context).username),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
readOnly: loading,
|
||||
autocorrect: false,
|
||||
autofillHints: loading ? null : [AutofillHints.password],
|
||||
controller: passwordController,
|
||||
obscureText: !showPassword,
|
||||
onSubmitted: (t) => login(context),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.lock_outlined),
|
||||
hintText: '****',
|
||||
errorText: passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context).showPassword,
|
||||
icon: Icon(showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined),
|
||||
onPressed: () =>
|
||||
setState(() => showPassword = !showPassword),
|
||||
),
|
||||
labelText: L10n.of(context).password),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed: loading ? null : () => login(context),
|
||||
child: loading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).login.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => _passwordForgotten(context),
|
||||
child: Text(
|
||||
L10n.of(context).passwordForgotten,
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
static final RegExp _phoneRegex =
|
||||
RegExp(r'^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$');
|
||||
bool get isEmail => EmailValidator.validate(this);
|
||||
bool get isPhoneNumber => _phoneRegex.hasMatch(this);
|
||||
}
|
||||
54
lib/pages/views/new_group_ui.dart
Normal file
54
lib/pages/views/new_group_ui.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:fluffychat/pages/new_group.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class NewGroupUI extends StatelessWidget {
|
||||
final NewGroupController controller;
|
||||
|
||||
const NewGroupUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).createNewGroup),
|
||||
elevation: 0,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.controller,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.go,
|
||||
onSubmitted: controller.submitAction,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).optionalGroupName,
|
||||
prefixIcon: Icon(Icons.people_outlined),
|
||||
hintText: L10n.of(context).enterAGroupName),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(L10n.of(context).groupIsPublic),
|
||||
value: controller.publicGroup,
|
||||
onChanged: controller.setPublicGroup,
|
||||
),
|
||||
Expanded(
|
||||
child: Image.asset('assets/new_group_wallpaper.png'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: controller.submitAction,
|
||||
child: Icon(Icons.arrow_forward_outlined),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
133
lib/pages/views/new_private_chat_ui.dart
Normal file
133
lib/pages/views/new_private_chat_ui.dart
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/new_private_chat.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/contacts_list.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
|
||||
class NewPrivateChatUI extends StatelessWidget {
|
||||
final NewPrivateChatController controller;
|
||||
|
||||
const NewPrivateChatUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).newChat),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => AdaptivePageLayout.of(context)
|
||||
.pushNamedAndRemoveUntilIsFirst('/newgroup'),
|
||||
child: Text(
|
||||
L10n.of(context).createNewGroup,
|
||||
style: TextStyle(color: Theme.of(context).accentColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: TextFormField(
|
||||
controller: controller.controller,
|
||||
autocorrect: false,
|
||||
onChanged: controller.searchUserWithCoolDown,
|
||||
textInputAction: TextInputAction.go,
|
||||
onFieldSubmitted: controller.submitAction,
|
||||
validator: controller.validateForm,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).enterAUsername,
|
||||
prefixIcon: controller.loading
|
||||
? Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: controller.correctMxId
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Avatar(
|
||||
controller.foundProfile.avatarUrl,
|
||||
controller.foundProfile.displayname ??
|
||||
controller.foundProfile.userId,
|
||||
size: 12,
|
||||
),
|
||||
)
|
||||
: Icon(Icons.account_circle_outlined),
|
||||
prefixText: '@',
|
||||
suffixIcon: IconButton(
|
||||
onPressed: controller.submitAction,
|
||||
icon: Icon(Icons.arrow_forward_outlined),
|
||||
),
|
||||
hintText: '${L10n.of(context).username.toLowerCase()}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
radius: Avatar.defaultSize / 2,
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
child: Icon(Icons.share_outlined),
|
||||
),
|
||||
onTap: controller.inviteAction,
|
||||
title: Text('${L10n.of(context).yourOwnUsername}:'),
|
||||
subtitle: Text(
|
||||
Matrix.of(context).client.userID,
|
||||
style: TextStyle(color: Theme.of(context).accentColor),
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
if (controller.foundProfiles.isNotEmpty)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final foundProfile = controller.foundProfiles[i];
|
||||
return ListTile(
|
||||
onTap: () => controller.pickUser(foundProfile),
|
||||
leading: Avatar(
|
||||
foundProfile.avatarUrl,
|
||||
foundProfile.displayname ?? foundProfile.userId,
|
||||
//size: 24,
|
||||
),
|
||||
title: Text(
|
||||
foundProfile.displayname ??
|
||||
foundProfile.userId.localpart,
|
||||
style: TextStyle(),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
foundProfile.userId,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (controller.foundProfiles.isEmpty)
|
||||
Expanded(
|
||||
child: ContactsList(searchController: controller.controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
272
lib/pages/views/search_ui.dart
Normal file
272
lib/pages/views/search_ui.dart
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/contacts_list.dart';
|
||||
import 'package:fluffychat/widgets/default_app_bar_search_field.dart';
|
||||
import 'package:fluffychat/widgets/list_items/chat_list_item.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../search.dart';
|
||||
|
||||
class SearchUI extends StatelessWidget {
|
||||
final SearchController controller;
|
||||
|
||||
const SearchUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final server = controller.genericSearchTerm?.isValidMatrixId ?? false
|
||||
? controller.genericSearchTerm.domain
|
||||
: controller.server;
|
||||
if (controller.lastServer != server) {
|
||||
controller.lastServer = server;
|
||||
controller.publicRoomsResponse = null;
|
||||
}
|
||||
controller.publicRoomsResponse ??= Matrix.of(context)
|
||||
.client
|
||||
.queryPublicRooms(
|
||||
server: server,
|
||||
genericSearchTerm: controller.genericSearchTerm,
|
||||
)
|
||||
.catchError((error) {
|
||||
if (controller.widget.alias == null) {
|
||||
throw error;
|
||||
}
|
||||
return PublicRoomsResponse.fromJson({
|
||||
'chunk': [],
|
||||
});
|
||||
}).then((PublicRoomsResponse res) {
|
||||
if (controller.widget.alias != null &&
|
||||
!res.chunk.any((room) =>
|
||||
(room.aliases?.contains(controller.widget.alias) ?? false) ||
|
||||
room.canonicalAlias == controller.widget.alias)) {
|
||||
// we have to tack on the original alias
|
||||
res.chunk.add(PublicRoom.fromJson(<String, dynamic>{
|
||||
'aliases': [controller.widget.alias],
|
||||
'name': controller.widget.alias,
|
||||
}));
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
final rooms = List<Room>.from(Matrix.of(context).client.rooms);
|
||||
rooms.removeWhere(
|
||||
(room) =>
|
||||
room.lastEvent == null ||
|
||||
!room.displayname
|
||||
.toLowerCase()
|
||||
.contains(controller.controller.text.toLowerCase()),
|
||||
);
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
initialIndex: 1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
titleSpacing: 0,
|
||||
title: DefaultAppBarSearchField(
|
||||
autofocus: true,
|
||||
hintText: L10n.of(context).search,
|
||||
searchController: controller.controller,
|
||||
suffix: Icon(Icons.search_outlined),
|
||||
onChanged: controller.search,
|
||||
),
|
||||
bottom: TabBar(
|
||||
indicatorColor: Theme.of(context).accentColor,
|
||||
labelColor: Theme.of(context).accentColor,
|
||||
unselectedLabelColor: Theme.of(context).textTheme.bodyText1.color,
|
||||
labelStyle: TextStyle(fontSize: 16),
|
||||
labelPadding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 0,
|
||||
),
|
||||
tabs: [
|
||||
Tab(child: Text(L10n.of(context).discover, maxLines: 1)),
|
||||
Tab(child: Text(L10n.of(context).chats, maxLines: 1)),
|
||||
Tab(child: Text(L10n.of(context).people, maxLines: 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
SizedBox(height: 12),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Theme.of(context).accentColor,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
child: Icon(Icons.edit_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).changeTheServer),
|
||||
onTap: controller.setServer,
|
||||
),
|
||||
FutureBuilder<PublicRoomsResponse>(
|
||||
future: controller.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: () => controller.joinGroupAction(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
ListView.builder(
|
||||
itemCount: rooms.length,
|
||||
itemBuilder: (_, i) => ChatListItem(rooms[i]),
|
||||
),
|
||||
controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final foundProfile = controller.foundProfiles[i];
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.startDirectChat(foundProfile.userId),
|
||||
);
|
||||
if (roomID.error == null) {
|
||||
await AdaptivePageLayout.of(context)
|
||||
.popAndPushNamed('/rooms/${roomID.result}');
|
||||
}
|
||||
},
|
||||
leading: Avatar(
|
||||
foundProfile.avatarUrl,
|
||||
foundProfile.displayname ?? foundProfile.userId,
|
||||
//size: 24,
|
||||
),
|
||||
title: Text(
|
||||
foundProfile.displayname ??
|
||||
foundProfile.userId.localpart,
|
||||
style: TextStyle(),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
foundProfile.userId,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ContactsList(searchController: controller.controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
104
lib/pages/views/settings_3pid_ui.dart
Normal file
104
lib/pages/views/settings_3pid_ui.dart
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/pages/settings_3pid.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class Settings3PidUI extends StatelessWidget {
|
||||
final Settings3PidController controller;
|
||||
|
||||
const Settings3PidUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.request ??= Matrix.of(context).client.getAccount3PIDs();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).passwordRecovery),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.add_outlined),
|
||||
onPressed: controller.add3PidAction,
|
||||
tooltip: L10n.of(context).addEmail,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: FutureBuilder<List<ThirdPartyIdentifier>>(
|
||||
future: controller.request,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<ThirdPartyIdentifier>> snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
snapshot.error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final identifier = snapshot.data;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor:
|
||||
identifier.isEmpty ? Colors.orange : Colors.grey,
|
||||
child: Icon(
|
||||
identifier.isEmpty
|
||||
? Icons.warning_outlined
|
||||
: Icons.info_outlined,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
identifier.isEmpty
|
||||
? L10n.of(context).noPasswordRecoveryDescription
|
||||
: L10n.of(context)
|
||||
.withTheseAddressesRecoveryDescription,
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: identifier.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(identifier[i].iconData)),
|
||||
title: Text(identifier[i].address),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context).delete,
|
||||
icon: Icon(Icons.delete_forever_outlined),
|
||||
color: Colors.red,
|
||||
onPressed: () => controller.delete3Pid(identifier[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on ThirdPartyIdentifier {
|
||||
IconData get iconData {
|
||||
switch (medium) {
|
||||
case ThirdPartyIdentifierMedium.email:
|
||||
return Icons.mail_outline_rounded;
|
||||
case ThirdPartyIdentifierMedium.msisdn:
|
||||
return Icons.phone_android_outlined;
|
||||
}
|
||||
return Icons.device_unknown_outlined;
|
||||
}
|
||||
}
|
||||
230
lib/pages/views/settings_emotes_ui.dart
Normal file
230
lib/pages/views/settings_emotes_ui.dart
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../settings_emotes.dart';
|
||||
|
||||
class EmotesSettingsUI extends StatelessWidget {
|
||||
final EmotesSettingsController controller;
|
||||
|
||||
const EmotesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).emoteSettings),
|
||||
),
|
||||
floatingActionButton: controller.showSave
|
||||
? FloatingActionButton(
|
||||
onPressed: controller.saveAction,
|
||||
child: Icon(Icons.save_outlined, color: Colors.white),
|
||||
)
|
||||
: null,
|
||||
body: MaxWidthBody(
|
||||
child: StreamBuilder(
|
||||
stream: controller.widget.room?.onUpdate?.stream,
|
||||
builder: (context, snapshot) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (!controller.readonly)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 180.0,
|
||||
height: 38,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.newEmoteController,
|
||||
autocorrect: false,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).emoteShortcode,
|
||||
prefixText: ': ',
|
||||
suffixText: ':',
|
||||
prefixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
suffixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: _EmoteImagePicker(
|
||||
controller: controller.newMxcController,
|
||||
onPressed: controller.emoteImagePickerAction,
|
||||
),
|
||||
trailing: InkWell(
|
||||
onTap: controller.addEmoteAction,
|
||||
child: Icon(
|
||||
Icons.add_outlined,
|
||||
color: Colors.green,
|
||||
size: 32.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.widget.room != null)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).enableEmotesGlobally),
|
||||
trailing: Switch(
|
||||
value: controller.isGloballyActive(client),
|
||||
onChanged: controller.setIsGloballyActive,
|
||||
),
|
||||
),
|
||||
if (!controller.readonly || controller.widget.room != null)
|
||||
Divider(
|
||||
height: 2,
|
||||
thickness: 2,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
Expanded(
|
||||
child: controller.emotes.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
L10n.of(context).noEmotesFound,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) =>
|
||||
Container(),
|
||||
itemCount: controller.emotes.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i >= controller.emotes.length) {
|
||||
return Container(height: 70);
|
||||
}
|
||||
final emote = controller.emotes[i];
|
||||
final textEditingController =
|
||||
TextEditingController();
|
||||
textEditingController.text = emote.emoteClean;
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 180.0,
|
||||
height: 38,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(10)),
|
||||
color:
|
||||
Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
child: TextField(
|
||||
readOnly: controller.readonly,
|
||||
controller: textEditingController,
|
||||
autocorrect: false,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context).emoteShortcode,
|
||||
prefixText: ': ',
|
||||
suffixText: ':',
|
||||
prefixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
suffixStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (s) =>
|
||||
controller.submitEmoteAction(
|
||||
s,
|
||||
emote,
|
||||
textEditingController,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: _EmoteImage(emote.mxc),
|
||||
trailing: controller.readonly
|
||||
? null
|
||||
: InkWell(
|
||||
onTap: () =>
|
||||
controller.removeEmoteAction(emote),
|
||||
child: Icon(
|
||||
Icons.delete_forever_outlined,
|
||||
color: Colors.red,
|
||||
size: 32.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmoteImage extends StatelessWidget {
|
||||
final String mxc;
|
||||
_EmoteImage(this.mxc);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = 38.0;
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final url = Uri.parse(mxc)?.getThumbnail(
|
||||
Matrix.of(context).client,
|
||||
width: size * devicePixelRatio,
|
||||
height: size * devicePixelRatio,
|
||||
method: ThumbnailMethod.scale,
|
||||
);
|
||||
return CachedNetworkImage(
|
||||
imageUrl: url.toString(),
|
||||
fit: BoxFit.contain,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmoteImagePicker extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
final void Function(TextEditingController) onPressed;
|
||||
|
||||
_EmoteImagePicker({@required this.controller, @required this.onPressed});
|
||||
|
||||
@override
|
||||
_EmoteImagePickerState createState() => _EmoteImagePickerState();
|
||||
}
|
||||
|
||||
class _EmoteImagePickerState extends State<_EmoteImagePicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.controller.text == null || widget.controller.text.isEmpty) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {},
|
||||
child: Text(L10n.of(context).pickImage),
|
||||
);
|
||||
} else {
|
||||
return _EmoteImage(widget.controller.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
lib/pages/views/settings_ignore_list_ui.dart
Normal file
94
lib/pages/views/settings_ignore_list_ui.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../settings_ignore_list.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class SettingsIgnoreListUI extends StatelessWidget {
|
||||
final SettingsIgnoreListController controller;
|
||||
|
||||
const SettingsIgnoreListUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).ignoredUsers),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.controller,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => controller.ignoreUser(context),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'bad_guy:domain.abc',
|
||||
prefixText: '@',
|
||||
labelText: L10n.of(context).ignoreUsername,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context).ignore,
|
||||
icon: Icon(Icons.done_outlined),
|
||||
onPressed: () => controller.ignoreUser(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
L10n.of(context).ignoreListDescription,
|
||||
style: TextStyle(color: Colors.orange),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: StreamBuilder<Object>(
|
||||
stream: client.onAccountData.stream
|
||||
.where((a) => a.type == 'm.ignored_user_list'),
|
||||
builder: (context, snapshot) {
|
||||
return ListView.builder(
|
||||
itemCount: client.ignoredUsers.length,
|
||||
itemBuilder: (c, i) => FutureBuilder<Profile>(
|
||||
future:
|
||||
client.getProfileFromUserId(client.ignoredUsers[i]),
|
||||
builder: (c, s) => ListTile(
|
||||
leading: Avatar(
|
||||
s.data?.avatarUrl ?? Uri.parse(''),
|
||||
s.data?.displayname ?? client.ignoredUsers[i],
|
||||
),
|
||||
title: Text(
|
||||
s.data?.displayname ?? client.ignoredUsers[i]),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context).delete,
|
||||
icon: Icon(Icons.delete_forever_outlined),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
client.unignoreUser(client.ignoredUsers[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
lib/pages/views/settings_multiple_emotes_ui.dart
Normal file
62
lib/pages/views/settings_multiple_emotes_ui.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/settings_multiple_emotes.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MultipleEmotesSettingsUI extends StatelessWidget {
|
||||
final MultipleEmotesSettingsController controller;
|
||||
|
||||
MultipleEmotesSettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room =
|
||||
Matrix.of(context).client.getRoomById(controller.widget.roomId);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).emotePacks),
|
||||
),
|
||||
body: StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
final packs =
|
||||
room.states['im.ponies.room_emotes'] ?? <String, Event>{};
|
||||
if (!packs.containsKey('')) {
|
||||
packs[''] = null;
|
||||
}
|
||||
final keys = packs.keys.toList();
|
||||
keys.sort();
|
||||
return ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) => Container(),
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final event = packs[keys[i]];
|
||||
var packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
||||
if (event != null && event.content['pack'] is Map) {
|
||||
if (event.content['pack']['displayname'] is String) {
|
||||
packName = event.content['pack']['displayname'];
|
||||
} else if (event.content['pack']['name'] is String) {
|
||||
packName = event.content['pack']['name'];
|
||||
}
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(packName),
|
||||
onTap: () async {
|
||||
await AdaptivePageLayout.of(context).pushNamed(
|
||||
'/settings/emotes',
|
||||
arguments: {
|
||||
'room': room,
|
||||
'stateKey': keys[i],
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
123
lib/pages/views/settings_notifications_ui.dart
Normal file
123
lib/pages/views/settings_notifications_ui.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
|
||||
import '../settings_notifications.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class SettingsNotificationsUI extends StatelessWidget {
|
||||
final SettingsNotificationsController controller;
|
||||
|
||||
const SettingsNotificationsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).notifications),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onAccountData
|
||||
.stream
|
||||
.where((event) => event.type == 'm.push_rules'),
|
||||
builder: (BuildContext context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: !Matrix.of(context).client.allPushNotificationsMuted,
|
||||
title: Text(
|
||||
L10n.of(context).notificationsEnabledForThisAccount),
|
||||
onChanged: (_) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).client.setMuteAllPushNotifications(
|
||||
!Matrix.of(context)
|
||||
.client
|
||||
.allPushNotificationsMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
|
||||
if (!kIsWeb && Platform.isAndroid)
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).soundVibrationLedColor),
|
||||
trailing: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(Icons.edit_outlined),
|
||||
),
|
||||
onTap: controller.openAndroidNotificationSettingsAction,
|
||||
),
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).pushRules,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (var item in NotificationSettingsItem.items)
|
||||
SwitchListTile(
|
||||
value: controller.getNotificationSetting(item) ?? true,
|
||||
title: Text(item.title(context)),
|
||||
onChanged: (bool enabled) =>
|
||||
controller.setNotificationSetting(item, enabled),
|
||||
),
|
||||
},
|
||||
Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).devices,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<List<Pusher>>(
|
||||
future: Matrix.of(context).client.getPushers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
Center(
|
||||
child: Text(
|
||||
snapshot.error.toLocalizedString(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final pushers = snapshot.data ?? [];
|
||||
return ListView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: pushers.length,
|
||||
itemBuilder: (_, i) => ListTile(
|
||||
title: Text(
|
||||
'${pushers[i].appDisplayName} - ${pushers[i].appId}'),
|
||||
subtitle: Text(pushers[i].data.url.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/pages/views/settings_style_ui.dart
Normal file
118
lib/pages/views/settings_style_ui.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../config/app_config.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../settings_style.dart';
|
||||
|
||||
class SettingsStyleUI extends StatelessWidget {
|
||||
final SettingsStyleController controller;
|
||||
|
||||
const SettingsStyleUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.currentTheme ??= AdaptiveTheme.of(context).mode;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(),
|
||||
title: Text(L10n.of(context).changeTheme),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<AdaptiveThemeMode>(
|
||||
groupValue: controller.currentTheme,
|
||||
value: AdaptiveThemeMode.system,
|
||||
title: Text(L10n.of(context).systemTheme),
|
||||
onChanged: controller.switchTheme,
|
||||
),
|
||||
RadioListTile<AdaptiveThemeMode>(
|
||||
groupValue: controller.currentTheme,
|
||||
value: AdaptiveThemeMode.light,
|
||||
title: Text(L10n.of(context).lightTheme),
|
||||
onChanged: controller.switchTheme,
|
||||
),
|
||||
RadioListTile<AdaptiveThemeMode>(
|
||||
groupValue: controller.currentTheme,
|
||||
value: AdaptiveThemeMode.dark,
|
||||
title: Text(L10n.of(context).darkTheme),
|
||||
onChanged: controller.switchTheme,
|
||||
),
|
||||
Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).wallpaper,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
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_outlined,
|
||||
color: Colors.red,
|
||||
),
|
||||
onTap: controller.deleteWallpaperAction,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context).changeWallpaper),
|
||||
trailing: Icon(Icons.wallpaper_outlined),
|
||||
onTap: controller.setWallpaperAction,
|
||||
);
|
||||
}),
|
||||
Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).fontSize,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text('(*${AppConfig.fontSizeFactor})'),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
|
||||
AppConfig.fontSizeFactor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
divisions: 4,
|
||||
value: AppConfig.fontSizeFactor,
|
||||
semanticFormatterCallback: (d) => d.toString(),
|
||||
onChanged: controller.changeFontSizeFactor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
261
lib/pages/views/settings_ui.dart
Normal file
261
lib/pages/views/settings_ui.dart
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/widgets/sentry_switch_list_tile.dart';
|
||||
import 'package:fluffychat/widgets/settings_switch_list_tile.dart';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/beautify_string_extension.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../widgets/content_banner.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../config/setting_keys.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class SettingsUI extends StatelessWidget {
|
||||
final SettingsController controller;
|
||||
|
||||
const SettingsUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
return Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) =>
|
||||
<Widget>[
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close_outlined),
|
||||
onPressed: () => AdaptivePageLayout.of(context).popUntilIsFirst(),
|
||||
),
|
||||
expandedHeight: 300.0,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
title: Text(L10n.of(context).settings),
|
||||
actions: [
|
||||
FutureBuilder(
|
||||
future: controller.crossSigningCachedFuture,
|
||||
builder: (context, snapshot) {
|
||||
final needsBootstrap = Matrix.of(context)
|
||||
.client
|
||||
.encryption
|
||||
?.crossSigning
|
||||
?.enabled ==
|
||||
false ||
|
||||
snapshot.data == false;
|
||||
final isUnknownSession =
|
||||
Matrix.of(context).client.isUnknownSession;
|
||||
final displayHeader = needsBootstrap || isUnknownSession;
|
||||
if (!displayHeader) return Container();
|
||||
return TextButton.icon(
|
||||
icon: Icon(Icons.cloud, color: Colors.red),
|
||||
label: Text(
|
||||
L10n.of(context).chatBackup,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onPressed: controller.firstRunBootstrapAction,
|
||||
);
|
||||
}),
|
||||
],
|
||||
backgroundColor: Theme.of(context).appBarTheme.color,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: ContentBanner(controller.profile?.avatarUrl,
|
||||
onEdit: controller.setAvatarAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).notifications,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
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(
|
||||
controller.profile?.displayname ?? client.userID.localpart),
|
||||
onTap: controller.setDisplaynameAction,
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.phone_outlined),
|
||||
title: Text(L10n.of(context).editJitsiInstance),
|
||||
subtitle: Text(AppConfig.jitsiInstance),
|
||||
onTap: controller.setJitsiInstanceAction,
|
||||
),
|
||||
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: controller.changePasswordAccountAction,
|
||||
),
|
||||
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: controller.logoutAction,
|
||||
),
|
||||
ListTile(
|
||||
trailing: Icon(Icons.delete_forever_outlined),
|
||||
title: Text(
|
||||
L10n.of(context).deleteAccount,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: controller.deleteAccountAction,
|
||||
),
|
||||
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: controller.setAppLockAction,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).yourPublicKey),
|
||||
onTap: () => showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).yourPublicKey,
|
||||
message: client.fingerprintKey.beautified,
|
||||
okLabel: L10n.of(context).ok,
|
||||
useRootNavigator: false,
|
||||
),
|
||||
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: controller.bootstrapSettingsAction,
|
||||
),
|
||||
},
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
lib/pages/views/sign_up_password_ui.dart
Normal file
87
lib/pages/views/sign_up_password_ui.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:fluffychat/pages/sign_up_password.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUpPasswordUI extends StatelessWidget {
|
||||
final SignUpPasswordController controller;
|
||||
|
||||
const SignUpPasswordUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: controller.loading ? Container() : BackButton(),
|
||||
title: Text(
|
||||
L10n.of(context).chooseAStrongPassword,
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.passwordController,
|
||||
obscureText: !controller.showPassword,
|
||||
autofocus: true,
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
onSubmitted: (_) => controller.signUpAction,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.newPassword],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.lock_outlined),
|
||||
hintText: '****',
|
||||
errorText: controller.passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context).showPassword,
|
||||
icon: Icon(controller.showPassword
|
||||
? Icons.visibility_off_outlined
|
||||
: Icons.visibility_outlined),
|
||||
onPressed: controller.toggleShowPassword,
|
||||
),
|
||||
labelText: L10n.of(context).password),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: controller.emailController,
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onSubmitted: (_) => controller.signUpAction,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.mail_outline_outlined),
|
||||
errorText: controller.emailError,
|
||||
hintText: 'email@example.com',
|
||||
labelText: L10n.of(context).optionalAddEmail),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.loading ? null : controller.signUpAction,
|
||||
child: controller.loading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).createAccountNow.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/pages/views/sign_up_ui.dart
Normal file
117
lib/pages/views/sign_up_ui.dart
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:fluffychat/pages/sign_up.dart';
|
||||
import 'package:fluffychat/widgets/fluffy_banner.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/layouts/one_page_card.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SignUpUI extends StatelessWidget {
|
||||
final SignUpController controller;
|
||||
|
||||
const SignUpUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnePageCard(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: controller.loading ? Container() : BackButton(),
|
||||
title: Text(
|
||||
Matrix.of(context)
|
||||
.client
|
||||
.homeserver
|
||||
.toString()
|
||||
.replaceFirst('https://', ''),
|
||||
),
|
||||
),
|
||||
body: ListView(children: <Widget>[
|
||||
Hero(
|
||||
tag: 'loginBanner',
|
||||
child: FluffyBanner(),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: TextField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
controller: controller.usernameController,
|
||||
onSubmitted: controller.signUpAction,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.newUsername],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.account_circle_outlined),
|
||||
hintText: L10n.of(context).username,
|
||||
errorText: controller.usernameError,
|
||||
labelText: L10n.of(context).chooseAUsername,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: controller.avatar == null
|
||||
? null
|
||||
: MemoryImage(controller.avatar.bytes),
|
||||
backgroundColor: controller.avatar == null
|
||||
? Theme.of(context).brightness == Brightness.dark
|
||||
? Color(0xff121212)
|
||||
: Colors.white
|
||||
: Theme.of(context).secondaryHeaderColor,
|
||||
child: controller.avatar == null
|
||||
? Icon(Icons.camera_alt_outlined,
|
||||
color: Theme.of(context).primaryColor)
|
||||
: null,
|
||||
),
|
||||
trailing: controller.avatar == null
|
||||
? null
|
||||
: Icon(
|
||||
Icons.close,
|
||||
color: Colors.red,
|
||||
),
|
||||
title: Text(controller.avatar == null
|
||||
? L10n.of(context).setAProfilePicture
|
||||
: L10n.of(context).discardPicture),
|
||||
onTap: controller.avatar == null
|
||||
? controller.setAvatarAction
|
||||
: controller.resetAvatarAction,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Hero(
|
||||
tag: 'loginButton',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.loading ? null : controller.signUpAction,
|
||||
child: controller.loading
|
||||
? LinearProgressIndicator()
|
||||
: Text(
|
||||
L10n.of(context).signUp.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
AdaptivePageLayout.of(context).pushNamed('/login'),
|
||||
child: Text(
|
||||
L10n.of(context).alreadyHaveAnAccount,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blue,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
157
lib/pages/views/user_bottom_sheet_ui.dart
Normal file
157
lib/pages/views/user_bottom_sheet_ui.dart
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../widgets/content_banner.dart';
|
||||
import '../user_bottom_sheet.dart';
|
||||
import '../../utils/presence_extension.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class UserBottomSheetUI extends StatelessWidget {
|
||||
final UserBottomSheetController controller;
|
||||
|
||||
const UserBottomSheetUI(this.controller, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = controller.widget.user;
|
||||
final client = user.room.client;
|
||||
final presence = client.presences[user.id];
|
||||
return Center(
|
||||
child: Container(
|
||||
width: min(
|
||||
MediaQuery.of(context).size.width, FluffyThemes.columnWidth * 1.5),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_downward_outlined),
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
tooltip: L10n.of(context).close,
|
||||
),
|
||||
title: Text(user.calcDisplayname()),
|
||||
actions: [
|
||||
if (user.id != user.room.client.userID)
|
||||
PopupMenuButton(
|
||||
itemBuilder: (_) => [
|
||||
if (controller.widget.onMention != null)
|
||||
PopupMenuItem(
|
||||
value: 'mention',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).mention,
|
||||
Icons.alternate_email_outlined,
|
||||
),
|
||||
),
|
||||
if (user.id != user.room.client.userID &&
|
||||
!user.room.isDirectChat)
|
||||
PopupMenuItem(
|
||||
value: 'message',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).sendAMessage,
|
||||
Icons.send_outlined,
|
||||
),
|
||||
),
|
||||
if (user.canChangePowerLevel)
|
||||
PopupMenuItem(
|
||||
value: 'permission',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).setPermissionsLevel,
|
||||
Icons.edit_attributes_outlined,
|
||||
),
|
||||
),
|
||||
if (user.canKick)
|
||||
PopupMenuItem(
|
||||
value: 'kick',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).kickFromChat,
|
||||
Icons.exit_to_app_outlined,
|
||||
),
|
||||
),
|
||||
if (user.canBan && user.membership != Membership.ban)
|
||||
PopupMenuItem(
|
||||
value: 'ban',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).banFromChat,
|
||||
Icons.warning_sharp,
|
||||
),
|
||||
)
|
||||
else if (user.canBan &&
|
||||
user.membership == Membership.ban)
|
||||
PopupMenuItem(
|
||||
value: 'unban',
|
||||
child: _TextWithIcon(
|
||||
L10n.of(context).removeExile,
|
||||
Icons.warning_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: controller.participantAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ContentBanner(
|
||||
user.avatarUrl,
|
||||
defaultIcon: Icons.person_outline,
|
||||
client: user.room.client,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).username),
|
||||
subtitle: Text(user.id),
|
||||
trailing: Icon(Icons.share_outlined),
|
||||
onTap: () => FluffyShare.share(
|
||||
user.id, controller.widget.outerContext),
|
||||
),
|
||||
if (presence != null)
|
||||
ListTile(
|
||||
title: Text(presence.getLocalizedStatusMessage(context)),
|
||||
subtitle:
|
||||
Text(presence.getLocalizedLastActiveAgo(context)),
|
||||
trailing: Icon(Icons.circle,
|
||||
color: presence.presence.currentlyActive ?? false
|
||||
? Colors.green
|
||||
: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextWithIcon extends StatelessWidget {
|
||||
final String text;
|
||||
final IconData iconData;
|
||||
|
||||
const _TextWithIcon(
|
||||
this.text,
|
||||
this.iconData, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(iconData),
|
||||
SizedBox(width: 16),
|
||||
Text(text),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue