Merge branch 'main' of https://github.com/krille-chan/fluffychat
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled

This commit is contained in:
Alexey 2026-04-23 10:59:25 +03:00
commit e12723fdaa
98 changed files with 1450 additions and 1014 deletions

View file

@ -48,6 +48,7 @@ class ArchiveController extends State<Archive> {
OkCancelResult.ok) {
return;
}
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
futureWithProgress: (onProgress) async {

View file

@ -382,6 +382,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
).wrongRecoveryKey,
);
} catch (e, s) {
if (!context.mounted) return;
ErrorReporter(
context,
'Unable to open SSSS with recovery key',
@ -425,6 +426,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
cancelLabel: L10n.of(context).cancel,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
final req = await showFutureLoadingDialog(
context: context,
delay: false,
@ -435,11 +437,12 @@ class BootstrapDialogState extends State<BootstrapDialog> {
},
);
if (req.error != null) return;
if (!context.mounted) return;
final success = await KeyVerificationDialog(
request: req.result!,
).show(context);
if (success != true) return;
if (!mounted) return;
if (!context.mounted) return;
final result = await showFutureLoadingDialog(
context: context,

View file

@ -212,6 +212,7 @@ class ChatController extends State<ChatPageWithRoom>
context: context,
future: room.leave,
);
if (!mounted) return;
if (success.error != null) return;
context.go('/rooms');
}
@ -463,21 +464,25 @@ class ChatController extends State<ChatPageWithRoom>
scrollUpBannerEventId = eventId;
});
bool firstUpdateReceived = false;
String? animateInEventId;
void _insert(int index) {
if (index > 0) return;
animateInEventId = timeline?.events.firstOrNull?.eventId;
}
void updateView() {
if (!mounted) return;
setReadMarker();
setState(() {
firstUpdateReceived = true;
});
setState(() {});
}
Future<void>? loadTimelineFuture;
Future<void> _getTimeline({String? eventContextId}) async {
await Matrix.of(context).client.roomsLoading;
await Matrix.of(context).client.accountDataLoading;
final matrix = Matrix.of(context);
await matrix.client.roomsLoading;
await matrix.client.accountDataLoading;
if (eventContextId != null &&
(!eventContextId.isValidMatrixId || eventContextId.sigil != '\$')) {
eventContextId = null;
@ -486,6 +491,7 @@ class ChatController extends State<ChatPageWithRoom>
timeline?.cancelSubscriptions();
timeline = await room.getTimeline(
onUpdate: updateView,
onInsert: _insert,
eventContextId: eventContextId,
);
} catch (e, s) {
@ -633,6 +639,7 @@ class ChatController extends State<ChatPageWithRoom>
Future<void> sendFileAction({FileType type = FileType.any}) async {
final files = await selectFiles(context, allowMultiple: true, type: type);
if (files.isEmpty) return;
if (!mounted) return;
await showAdaptiveDialog(
context: context,
builder: (c) => SendFileDialog(
@ -669,6 +676,7 @@ class ChatController extends State<ChatPageWithRoom>
FocusScope.of(context).requestFocus(FocusNode());
final file = await ImagePicker().pickImage(source: ImageSource.camera);
if (file == null) return;
if (!mounted) return;
await showAdaptiveDialog(
context: context,
@ -690,6 +698,7 @@ class ChatController extends State<ChatPageWithRoom>
maxDuration: const Duration(minutes: 1),
);
if (file == null) return;
if (!mounted) return;
await showAdaptiveDialog(
context: context,
@ -732,26 +741,27 @@ class ChatController extends State<ChatPageWithRoom>
mimeType: mimeType,
);
room
.sendFileEvent(
file,
inReplyTo: replyEvent,
threadRootEventId: activeThreadId,
extraContent: {
'info': {...file.info, 'duration': duration},
'org.matrix.msc3245.voice': {},
'org.matrix.msc1767.audio': {
'duration': duration,
'waveform': waveform,
},
try {
await room.sendFileEvent(
file,
inReplyTo: replyEvent,
threadRootEventId: activeThreadId,
extraContent: {
'info': {...file.info, 'duration': duration},
'org.matrix.msc3245.voice': {},
'org.matrix.msc1767.audio': {
'duration': duration,
'waveform': waveform,
},
)
.catchError((e) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text((e as Object).toLocalizedString(context))),
);
return null;
});
},
);
} catch (e) {
if (!mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(e.toLocalizedString(context))),
);
return;
}
setState(() {
replyEvent = null;
});
@ -813,29 +823,30 @@ class ChatController extends State<ChatPageWithRoom>
Future<void> reportEventAction() async {
final event = selectedEvents.single;
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final score = await showModalActionPopup<int>(
context: context,
title: L10n.of(context).reportMessage,
message: L10n.of(context).howOffensiveIsThisContent,
cancelLabel: L10n.of(context).cancel,
title: l10n.reportMessage,
message: l10n.howOffensiveIsThisContent,
cancelLabel: l10n.cancel,
actions: [
AdaptiveModalAction(
value: -100,
label: L10n.of(context).extremeOffensive,
),
AdaptiveModalAction(value: -50, label: L10n.of(context).offensive),
AdaptiveModalAction(value: 0, label: L10n.of(context).inoffensive),
AdaptiveModalAction(value: -100, label: l10n.extremeOffensive),
AdaptiveModalAction(value: -50, label: l10n.offensive),
AdaptiveModalAction(value: 0, label: l10n.inoffensive),
],
);
if (score == null) return;
if (!mounted) return;
final reason = await showTextInputDialog(
context: context,
title: L10n.of(context).whyDoYouWantToReportThis,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).reason,
title: l10n.whyDoYouWantToReportThis,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
hintText: l10n.reason,
);
if (reason == null || reason.isEmpty) return;
if (!mounted) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.reportEvent(
@ -846,12 +857,13 @@ class ChatController extends State<ChatPageWithRoom>
),
);
if (result.error != null) return;
if (!mounted) return;
setState(() {
showEmojiPicker = false;
selectedEvents.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).contentHasBeenReported)),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.contentHasBeenReported)),
);
}
@ -867,6 +879,7 @@ class ChatController extends State<ChatPageWithRoom>
}
setState(selectedEvents.clear);
} catch (e, s) {
if (!mounted) return;
ErrorReporter(
context,
'Error while delete error events action',
@ -891,6 +904,7 @@ class ChatController extends State<ChatPageWithRoom>
: null;
if (reasonInput == null) return;
final reason = reasonInput.isEmpty ? null : reasonInput;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
futureWithProgress: (onProgress) async {
@ -1147,7 +1161,7 @@ class ChatController extends State<ChatPageWithRoom>
true,
false,
);
users.sort((a, b) => a.powerLevel.compareTo(b.powerLevel));
users.sort((a, b) => a.powerLevel.level.compareTo(b.powerLevel.level));
final via = users
.map((user) => user.id.domain)
.whereType<String>()
@ -1248,6 +1262,7 @@ class ChatController extends State<ChatPageWithRoom>
okLabel: L10n.of(context).unpin,
cancelLabel: L10n.of(context).cancel,
);
if (!mounted) return;
if (response == OkCancelResult.ok) {
final events = room.pinnedEventIds
..removeWhere((oldEvent) => oldEvent == eventId);
@ -1337,17 +1352,18 @@ class ChatController extends State<ChatPageWithRoom>
Future<void> onPhoneButtonTap() async {
// VoIP required Android SDK 21
if (PlatformInfos.isAndroid) {
DeviceInfoPlugin().androidInfo.then((value) {
if (value.version.sdkInt < 21) {
Navigator.pop(context);
showOkAlertDialog(
context: context,
title: L10n.of(context).unsupportedAndroidVersion,
message: L10n.of(context).unsupportedAndroidVersionLong,
okLabel: L10n.of(context).close,
);
}
});
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (!mounted) return;
if (androidInfo.version.sdkInt < 21) {
Navigator.pop(context);
await showOkAlertDialog(
context: context,
title: L10n.of(context).unsupportedAndroidVersion,
message: L10n.of(context).unsupportedAndroidVersionLong,
okLabel: L10n.of(context).close,
);
return;
}
}
final callType = await showModalActionPopup<CallType>(
context: context,
@ -1368,11 +1384,13 @@ class ChatController extends State<ChatPageWithRoom>
],
);
if (callType == null) return;
if (!mounted) return;
final voipPlugin = Matrix.of(context).voipPlugin;
try {
await voipPlugin!.voip.inviteToCall(room, callType);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));

View file

@ -77,7 +77,7 @@ class ChatEventList extends StatelessWidget {
return Column(
mainAxisSize: .min,
children: [
SeenByRow(event: events.first),
if (events.isNotEmpty) SeenByRow(event: events.first),
TypingIndicators(controller),
],
);
@ -117,9 +117,7 @@ class ChatEventList extends StatelessWidget {
// The message at this index:
final event = events[i];
final animateIn =
event.eventId == timeline.events.first.eventId &&
controller.firstUpdateReceived;
final animateIn = event.eventId == controller.animateInEventId;
final nextEvent = i + 1 < events.length ? events[i + 1] : null;
final previousEvent = i > 0 ? events[i - 1] : null;

View file

@ -189,6 +189,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
});
} catch (e, s) {
Logs().v('Could not download audio file', e, s);
if (!mounted) rethrow;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
@ -208,6 +209,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
),
);
}
if (!mounted) return;
audioPlayer.play().onError(
ErrorReporter(context, 'Unable to play audio message').onErrorCallback,

View file

@ -50,6 +50,7 @@ class _CuteContentState extends State<CuteContent> {
Future<void> addOverlay() async {
_isOverlayShown = true;
await Future.delayed(const Duration(milliseconds: 50));
if (!mounted) return;
OverlayEntry? overlay;
overlay = OverlayEntry(

View file

@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/code_highlight_theme.dart';
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -509,11 +510,15 @@ class HtmlMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final element = parser.parse(html).body ?? dom.Element.html('');
final configuredMaxLines = AppSettings.messagePreviewMaxLines.value;
final maxLines = !limitHeight || configuredMaxLines <= 0
? null
: configuredMaxLines;
return Text.rich(
_renderHtml(element, context),
style: TextStyle(fontSize: fontSize, color: textColor),
maxLines: limitHeight ? 64 : null,
overflow: TextOverflow.fade,
maxLines: maxLines,
overflow: maxLines == null ? TextOverflow.visible : TextOverflow.fade,
selectionColor: textColor.withAlpha(128),
);
}

View file

@ -205,6 +205,8 @@ class Message extends StatelessWidget {
final enterThread = this.enterThread;
final sender = event.senderFromMemoryOrFallback;
final fileSendingStatus = event.fileSendingStatus;
return _AnimateIn(
animateIn: animateIn,
child: Center(
@ -318,9 +320,33 @@ class Message extends StatelessWidget {
height: 16,
child: event.status == EventStatus.error
? const Icon(Icons.error, color: Colors.red)
: event.fileSendingStatus != null
? const CircularProgressIndicator.adaptive(
strokeWidth: 1,
: fileSendingStatus != null
? Stack(
children: [
Center(
child: switch (fileSendingStatus) {
FileSendingStatus
.generatingThumbnail =>
Icon(
Icons.compress_outlined,
size: 14,
),
FileSendingStatus.encrypting =>
Icon(
Icons.lock_outlined,
size: 14,
),
FileSendingStatus.uploading =>
Icon(
Icons.upload_outlined,
size: 14,
),
},
),
const CircularProgressIndicator.adaptive(
strokeWidth: 1,
),
],
)
: null,
),
@ -361,17 +387,20 @@ class Message extends StatelessWidget {
? const SizedBox(height: 12)
: Row(
children: [
if (sender.powerLevel >= 50)
if (sender.powerLevel.role !=
PowerLevelRole.user)
Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(
sender.powerLevel >= 100
sender.powerLevel.role ==
PowerLevelRole
.moderator
? Icons
.admin_panel_settings
.add_moderator_outlined
: Icons
.add_moderator_outlined,
.admin_panel_settings,
size: 14,
color: theme
.colorScheme
@ -432,147 +461,161 @@ class Message extends StatelessWidget {
HapticFeedback.heavyImpact();
onSelect(event);
},
child: Container(
decoration: BoxDecoration(
color: noBubble
? Colors.transparent
: color,
borderRadius: borderRadius,
),
clipBehavior: Clip.antiAlias,
child: BubbleBackground(
colors: colors,
ignore:
noBubble ||
!ownMessage ||
MediaQuery.highContrastOf(context),
scrollController: scrollController,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
child: AnimatedOpacity(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
opacity:
event.status.isSending ||
event.type == EventTypes.Encrypted
? 0.5
: 1,
child: Container(
decoration: BoxDecoration(
color: noBubble
? Colors.transparent
: color,
borderRadius: borderRadius,
),
clipBehavior: Clip.antiAlias,
child: BubbleBackground(
colors: colors,
ignore:
noBubble ||
!ownMessage ||
MediaQuery.highContrastOf(context),
scrollController: scrollController,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
),
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
if (event.inReplyToEventId(
includingFallback: false,
) !=
null)
FutureBuilder<Event?>(
future: event.getReplyEvent(
timeline,
),
builder: (BuildContext context, snapshot) {
final replyEvent =
snapshot.hasData
? snapshot.data!
: Event(
eventId:
event
.inReplyToEventId() ??
'\$fake_event_id',
content: {
'msgtype': 'm.text',
'body': '...',
},
senderId:
event.senderId,
type:
'm.room.message',
room: event.room,
status:
EventStatus.sent,
originServerTs:
DateTime.now(),
);
return Padding(
padding:
const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
),
child: Material(
color: Colors.transparent,
borderRadius: ReplyContent
.borderRadius,
child: InkWell(
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
if (event.inReplyToEventId(
includingFallback: false,
) !=
null)
FutureBuilder<Event?>(
future: event.getReplyEvent(
timeline,
),
builder: (BuildContext context, snapshot) {
final replyEvent =
snapshot.hasData
? snapshot.data!
: Event(
eventId:
event
.inReplyToEventId() ??
'\$fake_event_id',
content: {
'msgtype':
'm.text',
'body': '...',
},
senderId:
event.senderId,
type:
'm.room.message',
room: event.room,
status: EventStatus
.sent,
originServerTs:
DateTime.now(),
);
return Padding(
padding:
const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
),
child: Material(
color:
Colors.transparent,
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent
.eventId,
child: InkWell(
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent
.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline: timeline,
),
),
),
),
);
},
),
MessageContent(
displayEvent,
textColor: textColor,
linkColor: linkColor,
onInfoTab: onInfoTab,
borderRadius: borderRadius,
timeline: timeline,
selected: selected,
bigEmojis: bigEmojis,
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
))
Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
);
},
),
child: Row(
mainAxisSize:
MainAxisSize.min,
spacing: 4.0,
children: [
Icon(
Icons.edit_outlined,
color: textColor
.withAlpha(164),
size: 14,
),
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
),
style: TextStyle(
MessageContent(
displayEvent,
textColor: textColor,
linkColor: linkColor,
onInfoTab: onInfoTab,
borderRadius: borderRadius,
timeline: timeline,
selected: selected,
bigEmojis: bigEmojis,
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
))
Padding(
padding:
const EdgeInsets.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisSize:
MainAxisSize.min,
spacing: 4.0,
children: [
Icon(
Icons.edit_outlined,
color: textColor
.withAlpha(164),
fontSize: 11,
size: 14,
),
),
],
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
),
style: TextStyle(
color: textColor
.withAlpha(164),
fontSize: 11,
),
),
],
),
),
),
],
],
),
),
),
),
@ -959,15 +1002,10 @@ class __AnimateInState extends State<_AnimateIn> {
});
});
}
return AnimatedOpacity(
return AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
opacity: _animationFinished ? 1 : 0,
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: _animationFinished ? widget.child : const SizedBox.shrink(),
),
child: _animationFinished ? widget.child : const SizedBox.shrink(),
);
}
}

View file

@ -61,9 +61,11 @@ class MessageContent extends StatelessWidget {
final client = Matrix.of(context).client;
final state = await client.getCryptoIdentityState();
if (!state.connected) {
if (!context.mounted) return;
final success = await context.push('/backup');
if (success != true) return;
}
if (!context.mounted) return;
event.requestKey();
final sender = event.senderFromMemoryOrFallback;
await showAdaptiveBottomSheet(

View file

@ -392,6 +392,10 @@ class InputBar extends StatelessWidget {
controller: controller,
focusNode: focusNode,
readOnly: readOnly,
onEditingComplete: () {
// To not lose focus on iOS:
// https://github.com/krille-chan/fluffychat/issues/2784
},
contextMenuBuilder: (c, e) => MarkdownContextBuilder(
editableTextState: e,
controller: controller,

View file

@ -15,6 +15,7 @@ class PinnedEvents extends StatelessWidget {
const PinnedEvents(this.controller, {super.key});
Future<void> _displayPinnedEventsDialog(BuildContext context) async {
final l10n = L10n.of(context);
final eventsResult = await showFutureLoadingDialog(
context: context,
future: () => Future.wait(
@ -25,13 +26,14 @@ class PinnedEvents extends StatelessWidget {
);
final events = eventsResult.result;
if (events == null) return;
if (!context.mounted) return;
final eventId = events.length == 1
? events.single?.eventId
: await showModalActionPopup<String>(
context: context,
title: L10n.of(context).pin,
cancelLabel: L10n.of(context).cancel,
title: l10n.pin,
cancelLabel: l10n.cancel,
actions: events
.map(
(event) => AdaptiveModalAction(
@ -39,7 +41,7 @@ class PinnedEvents extends StatelessWidget {
icon: const Icon(Icons.push_pin_outlined),
label:
event?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
MatrixLocals(l10n),
withSenderNamePrefix: true,
hideReply: true,
) ??

View file

@ -44,6 +44,7 @@ class RecordingViewModelState extends State<RecordingViewModel> {
room.client.getConfig(); // Preload server file configuration.
if (PlatformInfos.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
if (!mounted) return;
if (info.version.sdkInt < 19) {
showOkAlertDialog(
context: context,
@ -76,6 +77,7 @@ class RecordingViewModelState extends State<RecordingViewModel> {
final result = await audioRecorder.hasPermission();
if (result != true) {
if (!mounted) return;
showOkAlertDialog(
context: context,
title: L10n.of(context).oopsSomethingWentWrong,
@ -97,10 +99,12 @@ class RecordingViewModelState extends State<RecordingViewModel> {
),
path: path ?? '',
);
if (!mounted) return;
setState(() => duration = Duration.zero);
_subscribe();
} catch (e, s) {
Logs().w('Unable to start voice message recording', e, s);
if (!mounted) return;
showOkAlertDialog(
context: context,
title: L10n.of(context).oopsSomethingWentWrong,

View file

@ -146,6 +146,7 @@ class SendFileDialogState extends State<SendFileDialog> {
scaffoldMessenger.clearSnackBars();
} catch (e) {
scaffoldMessenger.clearSnackBars();
if (!mounted || !widget.outerContext.mounted) rethrow;
final theme = Theme.of(context);
scaffoldMessenger.showSnackBar(
SnackBar(

View file

@ -81,6 +81,7 @@ class SendLocationDialogState extends State<SendLocationDialog> {
context: context,
future: () => widget.room.sendLocation(body, uri),
);
if (!mounted) return;
Navigator.of(context, rootNavigator: false).pop();
}

View file

@ -44,6 +44,7 @@ class _StartPollBottomSheetState extends State<StartPollBottomSheet> {
maxSelections: _allowMultipleAnswers ? _answers.length : 1,
txid: _txid,
);
if (!mounted) return;
Navigator.of(context).pop();
} catch (e, s) {
Logs().w('Unable to create poll', e, s);

View file

@ -160,6 +160,7 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
}
Future<void> updateRoomAction() async {
final l10n = L10n.of(context);
final roomVersion = room
.getState(EventTypes.RoomCreate)!
.content
@ -170,10 +171,11 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
);
final capabilities = capabilitiesResult.result;
if (capabilities == null) return;
if (!mounted) return;
final newVersion = await showModalActionPopup<String>(
context: context,
title: L10n.of(context).replaceRoomWithNewerVersion,
cancelLabel: L10n.of(context).cancel,
title: l10n.replaceRoomWithNewerVersion,
cancelLabel: l10n.cancel,
actions: capabilities.mRoomVersions!.available.entries
.where((r) => r.key != roomVersion)
.map(
@ -185,18 +187,20 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
)
.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,
message: L10n.of(context).roomUpgradeDescription,
isDestructive: true,
)) {
if (newVersion == null) return;
if (!mounted) return;
final confirmUpgrade = await showOkCancelAlertDialog(
context: context,
okLabel: l10n.yes,
cancelLabel: l10n.cancel,
title: l10n.areYouSure,
message: l10n.roomUpgradeDescription,
isDestructive: true,
);
if (confirmUpgrade == OkCancelResult.cancel) {
return;
}
if (!mounted) return;
final result = await showFutureLoadingDialog(
context: context,
futureWithProgress: (onProgress) async {
@ -243,6 +247,7 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
}
Future<void> addAlias() async {
final l10n = L10n.of(context);
final domain = room.client.userID?.domain;
if (domain == null) {
throw Exception('userID or domain is null! This should never happen.');
@ -250,11 +255,12 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).editRoomAliases,
title: l10n.editRoomAliases,
prefixText: '#',
suffixText: domain,
hintText: L10n.of(context).alias,
hintText: l10n.alias,
);
if (!mounted) return;
final aliasLocalpart = input?.trim();
if (aliasLocalpart == null || aliasLocalpart.isEmpty) return;
final alias = '#$aliasLocalpart:$domain';
@ -264,17 +270,19 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
future: () => room.client.setRoomAlias(alias, room.id),
);
if (result.error != null) return;
if (!mounted) return;
setState(() {});
if (!room.canChangeStateEvent(EventTypes.RoomCanonicalAlias)) return;
final canonicalAliasConsent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).setAsCanonicalAlias,
title: l10n.setAsCanonicalAlias,
message: alias,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
okLabel: l10n.yes,
cancelLabel: l10n.no,
);
if (!mounted) return;
final altAliases =
room

View file

@ -37,69 +37,78 @@ class ChatDetailsController extends State<ChatDetails> {
String? get roomId => widget.roomId;
Future<void> setDisplaynameAction() async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final room = Matrix.of(context).client.getRoomById(roomId!)!;
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).changeTheNameOfTheGroup,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
initialText: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
title: l10n.changeTheNameOfTheGroup,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
initialText: room.getLocalizedDisplayname(MatrixLocals(l10n)),
);
if (input == null) return;
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setName(input),
);
if (!mounted) return;
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).displaynameHasBeenChanged)),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.displaynameHasBeenChanged)),
);
}
}
Future<void> setTopicAction() async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final room = Matrix.of(context).client.getRoomById(roomId!)!;
final input = await showTextInputDialog(
context: context,
title: L10n.of(context).setChatDescription,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).noChatDescriptionYet,
title: l10n.setChatDescription,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
hintText: l10n.noChatDescriptionYet,
initialText: room.topic,
minLines: 4,
maxLines: 8,
);
if (input == null) return;
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.setDescription(input),
);
if (!mounted) return;
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).chatDescriptionHasBeenChanged)),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.chatDescriptionHasBeenChanged)),
);
}
}
Future<void> setAvatarAction() async {
final l10n = L10n.of(context);
final room = Matrix.of(context).client.getRoomById(roomId!);
final actions = [
if (PlatformInfos.isMobile)
AdaptiveModalAction(
value: AvatarAction.camera,
label: L10n.of(context).openCamera,
label: l10n.openCamera,
isDefaultAction: true,
icon: const Icon(Icons.camera_alt_outlined),
),
AdaptiveModalAction(
value: AvatarAction.file,
label: L10n.of(context).openGallery,
label: l10n.openGallery,
icon: const Icon(Icons.photo_outlined),
),
if (room?.avatar != null)
AdaptiveModalAction(
value: AvatarAction.remove,
label: L10n.of(context).delete,
label: l10n.delete,
isDestructive: true,
icon: const Icon(Icons.delete_outlined),
),
@ -108,11 +117,12 @@ class ChatDetailsController extends State<ChatDetails> {
? actions.single.value
: await showModalActionPopup<AvatarAction>(
context: context,
title: L10n.of(context).editRoomAvatar,
cancelLabel: L10n.of(context).cancel,
title: l10n.editRoomAvatar,
cancelLabel: l10n.cancel,
actions: actions,
);
if (action == null) return;
if (!mounted) return;
if (action == AvatarAction.remove) {
await showFutureLoadingDialog(
context: context,
@ -131,6 +141,7 @@ class ChatDetailsController extends State<ChatDetails> {
if (result == null) return;
file = MatrixFile(bytes: await result.readAsBytes(), name: result.path);
} else {
if (!mounted) return;
final picked = await selectFiles(
context,
allowMultiple: false,
@ -143,6 +154,7 @@ class ChatDetailsController extends State<ChatDetails> {
name: pickedFile.name,
);
}
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => room!.setAvatar(file),

View file

@ -44,7 +44,7 @@ class ChatDetailsView extends StatelessWidget {
),
builder: (context, snapshot) {
var members = room.getParticipants().toList()
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
..sort((b, a) => a.powerLevel.level.compareTo(b.powerLevel.level));
members = members.take(10).toList();
final actualMembersCount =
(room.summary.mInvitedMemberCount ?? 0) +

View file

@ -23,13 +23,16 @@ class ParticipantListItem extends StatelessWidget {
Membership.leave => L10n.of(context).leftTheChat,
};
final permissionBatch = user.room.creatorUserIds.contains(user.id)
? L10n.of(context).owner
: user.powerLevel >= 100
? L10n.of(context).admin
: user.powerLevel >= 50
? L10n.of(context).moderator
: '';
final permissionBatch = switch (user.powerLevel.role) {
PowerLevelRole.user => '',
PowerLevelRole.moderator => L10n.of(context).moderator,
PowerLevelRole.admin => L10n.of(context).admin,
PowerLevelRole.owner => L10n.of(context).owner,
};
final isAdminOrOwner =
user.powerLevel.role == PowerLevelRole.admin ||
user.powerLevel.role == PowerLevelRole.owner;
return ListTile(
onTap: () => showMemberActionsPopupMenu(context: context, user: user),
@ -45,7 +48,7 @@ class ParticipantListItem extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: user.powerLevel >= 100
color: isAdminOrOwner
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
@ -53,7 +56,7 @@ class ParticipantListItem extends StatelessWidget {
child: Text(
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: user.powerLevel >= 100
color: isAdminOrOwner
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),

View file

@ -30,38 +30,40 @@ class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
}
Future<void> enableEncryption(_) async {
final l10n = L10n.of(context);
if (room.encrypted) {
showOkAlertDialog(
context: context,
title: L10n.of(context).sorryThatsNotPossible,
message: L10n.of(context).disableEncryptionWarning,
title: l10n.sorryThatsNotPossible,
message: l10n.disableEncryptionWarning,
);
return;
}
if (room.joinRules == JoinRules.public) {
showOkAlertDialog(
context: context,
title: L10n.of(context).sorryThatsNotPossible,
message: L10n.of(context).noEncryptionForPublicRooms,
title: l10n.sorryThatsNotPossible,
message: l10n.noEncryptionForPublicRooms,
);
return;
}
if (!room.canChangeStateEvent(EventTypes.Encryption)) {
showOkAlertDialog(
context: context,
title: L10n.of(context).sorryThatsNotPossible,
message: L10n.of(context).noPermission,
title: l10n.sorryThatsNotPossible,
message: l10n.noPermission,
);
return;
}
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).enableEncryptionWarning,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).cancel,
title: l10n.areYouSure,
message: l10n.enableEncryptionWarning,
okLabel: l10n.yes,
cancelLabel: l10n.cancel,
);
if (consent != OkCancelResult.ok) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => room.enableEncryption(),
@ -69,14 +71,16 @@ class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
}
Future<void> startVerification() async {
final l10n = L10n.of(context);
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).verifyOtherUser,
message: L10n.of(context).verifyOtherUserDescription,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.verifyOtherUser,
message: l10n.verifyOtherUserDescription,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
);
if (consent != OkCancelResult.ok) return;
if (!mounted) return;
final req = await room.client.userDeviceKeys[room.directChatMatrixID]!
.startVerification();
req.onUpdate = () {
@ -84,6 +88,7 @@ class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
setState(() {});
}
};
if (!mounted) return;
await KeyVerificationDialog(request: req).show(context);
}

View file

@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:cross_file/cross_file.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
@ -30,7 +30,7 @@ import '../../config/setting_keys.dart';
import '../../utils/url_launcher.dart';
import '../../widgets/matrix.dart';
enum ActiveFilter { allChats, messages, groups, unread, spaces }
enum ActiveFilter { allChats, messages, groups, unread, spaces, tag }
extension LocalizedActiveFilter on ActiveFilter {
String toLocalizedString(BuildContext context) {
@ -45,6 +45,8 @@ extension LocalizedActiveFilter on ActiveFilter {
return L10n.of(context).groups;
case ActiveFilter.spaces:
return L10n.of(context).spaces;
case ActiveFilter.tag:
throw 'Tags should not directly be displayed!';
}
}
}
@ -73,6 +75,7 @@ class ChatListController extends State<ChatList>
StreamSubscription? _intentFileStreamSubscription;
late ActiveFilter activeFilter;
String? activeTag;
String? _activeSpaceId;
String? get activeSpaceId => _activeSpaceId;
@ -90,6 +93,8 @@ class ChatListController extends State<ChatList>
});
Future<void> onChatTap(Room room) async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
if (room.membership == Membership.invite) {
final joinResult = await showFutureLoadingDialog(
context: context,
@ -105,10 +110,11 @@ class ChatListController extends State<ChatList>
);
if (joinResult.error != null) return;
}
if (!mounted) return;
if (room.membership == Membership.ban) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).youHaveBeenBannedFromThisChat)),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.youHaveBeenBannedFromThisChat)),
);
return;
}
@ -138,6 +144,8 @@ class ChatListController extends State<ChatList>
return (room) => room.isUnreadOrInvited;
case ActiveFilter.spaces:
return (room) => room.isSpace;
case ActiveFilter.tag:
return (room) => room.tags.keys.contains(activeTag);
}
}
@ -156,23 +164,25 @@ class ChatListController extends State<ChatList>
static const String _serverStoreNamespace = 'im.fluffychat.search.server';
Future<void> setServer() async {
final matrix = Matrix.of(context);
final l10n = L10n.of(context);
final newServer = await showTextInputDialog(
useRootNavigator: false,
title: L10n.of(context).changeTheHomeserver,
title: l10n.changeTheHomeserver,
context: context,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
prefixText: 'https://',
hintText: Matrix.of(context).client.homeserver?.host,
hintText: matrix.client.homeserver?.host,
initialText: searchServer,
keyboardType: TextInputType.url,
autocorrect: false,
validator: (server) => server.contains('.') == true
? null
: L10n.of(context).invalidServerName,
validator: (server) =>
server.contains('.') == true ? null : l10n.invalidServerName,
);
if (newServer == null) return;
Matrix.of(context).store.setString(_serverStoreNamespace, newServer);
if (!mounted) return;
matrix.store.setString(_serverStoreNamespace, newServer);
setState(() {
searchServer = newServer;
});
@ -185,6 +195,7 @@ class ChatListController extends State<ChatList>
Future<void> _search() async {
final client = Matrix.of(context).client;
final scaffoldMessenger = ScaffoldMessenger.of(context);
if (!isSearching) {
setState(() {
isSearching = true;
@ -227,9 +238,10 @@ class ChatListController extends State<ChatList>
);
} catch (e, s) {
Logs().w('Searching has crashed', e, s);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
if (!mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(e.toLocalizedString(context))),
);
}
if (!isSearchMode) return;
setState(() {
@ -293,9 +305,8 @@ class ChatListController extends State<ChatList>
Future<void> editSpace(BuildContext context, String spaceId) async {
await Matrix.of(context).client.getRoomById(spaceId)!.postLoad();
if (mounted) {
context.push('/rooms/$spaceId/details');
}
if (!context.mounted) return;
context.push('/rooms/$spaceId/details');
}
// Needs to match GroupsSpacesEntry for 'separate group' checking.
@ -305,11 +316,10 @@ class ChatListController extends State<ChatList>
String? get activeChat => widget.activeChat;
void _processIncomingSharedMedia(List<SharedMediaFile> files) {
files.removeWhere(
(file) => file.path.startsWith(AppConfig.deepLinkPrefix) == true,
);
if (files.isEmpty) return;
inspect(files);
if (files.singleOrNull?.path.startsWith(AppConfig.deepLinkPrefix) == true) {
return;
}
showScaffoldDialog(
context: context,
@ -353,9 +363,10 @@ class ChatListController extends State<ChatList>
}
}
StreamSubscription? _onRoomTagUpdate;
@override
void initState() {
activeFilter = ActiveFilter.allChats;
_initReceiveSharingIntent();
_activeSpaceId = widget.activeSpace;
@ -378,6 +389,32 @@ class ChatListController extends State<ChatList>
);
});
_updateRoomTags();
_onRoomTagUpdate = Matrix.of(context).client.onSync.stream
.where(
(syncUpdate) =>
syncUpdate.rooms?.join?.values.any(
(roomUpdate) =>
roomUpdate.accountData?.any(
(accountData) => accountData.type == 'm.tag',
) ??
false,
) ??
false,
)
.listen(_updateRoomTags);
if (roomTags.containsKey(AppSettings.chatFilter.value)) {
activeFilter = ActiveFilter.tag;
activeTag = AppSettings.chatFilter.value;
} else {
activeFilter =
ActiveFilter.values.singleWhereOrNull(
(filter) => AppSettings.chatFilter.value == filter.name,
) ??
ActiveFilter.allChats;
}
super.initState();
}
@ -385,6 +422,7 @@ class ChatListController extends State<ChatList>
void dispose() {
_intentDataStreamSubscription?.cancel();
_intentFileStreamSubscription?.cancel();
_onRoomTagUpdate?.cancel();
scrollController.removeListener(_onScroll);
super.dispose();
}
@ -613,6 +651,30 @@ class ChatListController extends State<ChatList>
],
),
),
if (activeTag == null)
PopupMenuItem(
value: ChatContextAction.addTag,
child: Row(
mainAxisSize: .min,
children: [
Icon(Icons.bookmark_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).addTag),
],
),
)
else
PopupMenuItem(
value: ChatContextAction.removeTag,
child: Row(
mainAxisSize: .min,
children: [
Icon(Icons.bookmark_remove_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).removeTag),
],
),
),
if (spacesWithPowerLevels.isNotEmpty)
PopupMenuItem(
value: ChatContextAction.addToSpace,
@ -742,6 +804,7 @@ class ChatListController extends State<ChatList>
.toList(),
);
if (space == null) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => space.setSpaceChild(room.id),
@ -752,9 +815,68 @@ class ChatListController extends State<ChatList>
future: () => room.setLowPriority(!room.isLowPriority),
);
return;
case ChatContextAction.addTag:
final existingTags = List.of(roomTags.keys);
existingTags.removeWhere(room.tags.containsKey);
String? tag;
if (existingTags.isNotEmpty) {
tag = await showModalActionPopup<String?>(
context: context,
actions: [
...existingTags.map((tag) {
final displayTag = tag.replaceFirst('u.', '');
return AdaptiveModalAction(
label: displayTag,
value: displayTag,
);
}),
AdaptiveModalAction(
label: L10n.of(context).createNewTag,
value: null,
),
],
);
if (!mounted) return;
}
tag ??= await showTextInputDialog(
context: context,
title: L10n.of(context).addTag,
hintText: L10n.of(context).tagName,
);
final newTag = tag;
if (!mounted) return;
if (newTag == null) return;
await showFutureLoadingDialog(
context: context,
future: () => room.addTag('u.$newTag'),
);
return;
case ChatContextAction.removeTag:
await showFutureLoadingDialog(
context: context,
future: () => room.removeTag(activeTag!),
);
return;
}
}
Map<String, int> roomTags = {};
void _updateRoomTags([_]) {
roomTags.clear();
for (final room in Matrix.of(context).client.rooms) {
for (final tag in room.tags.keys) {
if (tag.startsWith('u.')) roomTags[tag] = (roomTags[tag] ?? 0) + 1;
}
}
setState(() {
if (activeTag != null && !roomTags.keys.contains(activeTag)) {
activeTag = null;
activeFilter = ActiveFilter.allChats;
}
});
}
Future<void> dismissStatusList() async {
final result = await showOkCancelAlertDialog(
title: L10n.of(context).hidePresences,
@ -767,16 +889,18 @@ class ChatListController extends State<ChatList>
}
Future<void> setStatus() async {
final l10n = L10n.of(context);
final client = Matrix.of(context).client;
final currentPresence = await client.fetchCurrentPresence(client.userID!);
if (!mounted) return;
final input = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).setStatus,
message: L10n.of(context).leaveEmptyToClearStatus,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).statusExampleMessage,
title: l10n.setStatus,
message: l10n.leaveEmptyToClearStatus,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
hintText: l10n.statusExampleMessage,
maxLines: 6,
minLines: 1,
maxLength: 255,
@ -846,10 +970,17 @@ class ChatListController extends State<ChatList>
}
}
void setActiveFilter(ActiveFilter filter) {
void setActiveFilter(ActiveFilter filter, String? tag) {
if (filter == ActiveFilter.tag && tag == null) {
throw ('Must set a tag when setting filter to tags!');
}
setState(() {
activeTag = tag;
activeFilter = filter;
});
AppSettings.chatFilter.setItem(
filter == ActiveFilter.tag ? tag! : filter.name,
);
}
void setActiveClient(Client client) {
@ -904,18 +1035,21 @@ class ChatListController extends State<ChatList>
if (action == null) return;
switch (action) {
case EditBundleAction.addToBundle:
if (!mounted) return;
final bundle = await showTextInputDialog(
context: context,
title: l10n.bundleName,
hintText: l10n.bundleName,
);
if (bundle == null || bundle.isEmpty || bundle.isEmpty) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => client.setAccountBundle(bundle),
);
break;
case EditBundleAction.removeFromBundle:
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => client.removeFromAccountBundle(activeBundle!),
@ -962,6 +1096,8 @@ enum ChatContextAction {
goToSpace,
favorite,
lowPriority,
addTag,
removeTag,
markUnread,
mute,
leave,

View file

@ -134,37 +134,40 @@ class ChatListViewBody extends StatelessWidget {
padding: const EdgeInsets.all(12.0),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children:
[
ActiveFilter.allChats,
if (spaces.isNotEmpty &&
!AppSettings
.displayNavigationRail
.value &&
!FluffyThemes.isColumnMode(context))
ActiveFilter.spaces,
ActiveFilter.unread,
ActiveFilter.groups,
ActiveFilter.messages,
]
.map(
(filter) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected:
filter == controller.activeFilter,
onSelected: (_) =>
controller.setActiveFilter(filter),
label: Text(
filter.toLocalizedString(context),
),
children: [
...ActiveFilter.values
.where((filter) => filter != ActiveFilter.tag)
.map(
(filter) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected: filter == controller.activeFilter,
onSelected: (_) => controller
.setActiveFilter(filter, null),
label: Text(
filter.toLocalizedString(context),
),
),
)
.toList(),
),
),
...controller.roomTags.entries.map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected: entry.key == controller.activeTag,
onSelected: (_) => controller.setActiveFilter(
ActiveFilter.tag,
entry.key,
),
label: Text(entry.key.replaceFirst('u.', '')),
),
),
),
],
),
),
if (controller.isSearchMode)

View file

@ -219,7 +219,15 @@ class ChatListItem extends StatelessWidget {
room.latestEventReceivedTime.localizedTimeShort(
context,
),
style: TextStyle(fontSize: 11),
style: TextStyle(
fontSize: 11,
fontWeight: room.hasNewMessages
? FontWeight.bold
: null,
color: hasNotifications
? theme.colorScheme.primary
: null,
),
),
),
],

View file

@ -209,6 +209,7 @@ class ClientChooserButton extends StatelessWidget {
cancelLabel: L10n.of(context).cancel,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
context.go('/rooms/settings/addaccount');
break;
case SettingsAction.newGroup:

View file

@ -170,14 +170,17 @@ class _SpaceViewState extends State<SpaceView> {
switch (action) {
case SpaceActions.settings:
await space?.postLoad();
if (!mounted) return;
context.push('/rooms/${widget.spaceId}/details');
break;
case SpaceActions.invite:
await space?.postLoad();
if (!mounted) return;
context.push('/rooms/${widget.spaceId}/invite');
break;
case SpaceActions.members:
await space?.postLoad();
if (!mounted) return;
context.push('/rooms/${widget.spaceId}/details/members');
break;
case SpaceActions.leave:
@ -524,14 +527,16 @@ class _SpaceViewState extends State<SpaceView> {
);
}
final item = _discoveredChildren[i];
var joinedRoom = room.client.getRoomById(item.roomId);
final displayname =
item.name ??
item.canonicalAlias ??
joinedRoom?.getLocalizedDisplayname() ??
L10n.of(context).emptyChat;
final avatarUrl = item.avatarUrl ?? joinedRoom?.avatar;
if (!displayname.toLowerCase().contains(filter)) {
return const SizedBox.shrink();
}
var joinedRoom = room.client.getRoomById(item.roomId);
if (joinedRoom?.membership == Membership.leave) {
joinedRoom = null;
}
@ -595,7 +600,7 @@ class _SpaceViewState extends State<SpaceView> {
)
: Avatar(
size: avatarSize,
mxContent: item.avatarUrl,
mxContent: avatarUrl,
name: '#',
backgroundColor:
theme.colorScheme.surfaceContainer,

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/matrix.dart';
@ -39,7 +38,7 @@ class ChatMembersController extends State<ChatMembersPage> {
if (filter.isEmpty) {
setState(() {
filteredMembers = members
?..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
?..sort((b, a) => a.powerLevel.level.compareTo(b.powerLevel.level));
});
return;
}
@ -52,7 +51,7 @@ class ChatMembersController extends State<ChatMembersPage> {
user.id.toLowerCase().contains(filter),
)
.toList()
?..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
?..sort((b, a) => a.powerLevel.level.compareTo(b.powerLevel.level));
});
}

View file

@ -36,6 +36,7 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
currentLevel: currentLevel,
);
if (newLevel == null) return;
if (!context.mounted) return;
final content = Map<String, dynamic>.from(
room.getState(EventTypes.RoomPowerLevels)!.content,
);

View file

@ -41,12 +41,15 @@ class DevicesSettingsController extends State<DevicesSettings> {
Future<void> _checkChatBackup() async {
final client = Matrix.of(context).client;
final state = await client.getCryptoIdentityState();
if (!mounted) return;
setState(() {
chatBackupEnabled = state.initialized && !state.connected;
});
}
Future<void> removeDevicesAction(List<Device> devices) async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final client = Matrix.of(context).client;
final wellKnown = await Result.capture(client.getWellknown());
@ -57,18 +60,19 @@ class DevicesSettingsController extends State<DevicesSettings> {
launchUrlString(accountManageUrl, mode: LaunchMode.inAppBrowserView);
return;
}
if (!mounted) return;
if (await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).remove,
cancelLabel: L10n.of(context).cancel,
message: L10n.of(context).removeDevicesDescription,
title: l10n.areYouSure,
okLabel: l10n.remove,
cancelLabel: l10n.cancel,
message: l10n.removeDevicesDescription,
isDestructive: true,
) ==
OkCancelResult.cancel) {
return;
}
final matrix = Matrix.of(context);
if (!mounted) return;
final deviceIds = <String>[];
for (final userDevice in devices) {
deviceIds.add(userDevice.deviceId);
@ -85,19 +89,21 @@ class DevicesSettingsController extends State<DevicesSettings> {
}
Future<void> renameDeviceAction(Device device) async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final displayName = await showTextInputDialog(
context: context,
title: L10n.of(context).changeDeviceName,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.changeDeviceName,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
hintText: device.displayName,
);
if (displayName == null) return;
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(
context,
).client.updateDevice(device.deviceId, displayName: displayName),
future: () =>
matrix.client.updateDevice(device.deviceId, displayName: displayName),
);
if (success.error == null) {
reload();
@ -105,17 +111,20 @@ class DevicesSettingsController extends State<DevicesSettings> {
}
Future<void> verifyDeviceAction(Device device) async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).verifyOtherDevice,
message: L10n.of(context).verifyOtherDeviceDescription,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.verifyOtherDevice,
message: l10n.verifyOtherDeviceDescription,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
);
if (consent != OkCancelResult.ok) return;
final req = await Matrix.of(context)
if (!mounted) return;
final req = await matrix
.client
.userDeviceKeys[Matrix.of(context).client.userID!]!
.userDeviceKeys[matrix.client.userID!]!
.deviceKeys[device.deviceId]!
.startVerification();
req.onUpdate = () {
@ -126,6 +135,7 @@ class DevicesSettingsController extends State<DevicesSettings> {
setState(() {});
}
};
if (!mounted) return;
await KeyVerificationDialog(request: req).show(context);
}

View file

@ -81,10 +81,12 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
);
});
} on IOException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
} catch (e, s) {
if (!mounted) return;
ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s);
}
}

View file

@ -4,6 +4,7 @@ import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
Future<void> restoreBackupFlow(BuildContext context) async {
final matrix = Matrix.of(context);
final picked = await selectFiles(context);
final file = picked.firstOrNull;
if (file == null) return;
@ -12,9 +13,9 @@ Future<void> restoreBackupFlow(BuildContext context) async {
await showFutureLoadingDialog(
context: context,
future: () async {
final client = await Matrix.of(context).getLoginClient();
final client = await matrix.getLoginClient();
await client.importDump(String.fromCharCodes(await file.readAsBytes()));
Matrix.of(context).initMatrix();
matrix.initMatrix();
},
);
}

View file

@ -166,6 +166,7 @@ class IntroPage extends StatelessWidget {
final client = await Matrix.of(
context,
).getLoginClient();
if (!context.mounted) return;
context.go(
'${GoRouterState.of(context).uri.path}/login',
extra: client,

View file

@ -75,7 +75,8 @@ class _IntroPagePresenterState extends State<IntroPagePresenter> {
final client = await Matrix.of(context).getLoginClient();
await client.checkHomeserver(homeserverUrl);
await client.oidcLogin(session: session, code: code, state: state);
if (context.mounted) context.go('/backup');
if (!mounted) return;
context.go('/backup');
} catch (e, s) {
Logs().w('Unable to login via OIDC', e, s);
if (mounted) {

View file

@ -54,6 +54,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
String id,
String displayname,
) async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final room = Matrix.of(context).client.getRoomById(roomId!)!;
final success = await showFutureLoadingDialog(
@ -61,10 +63,9 @@ class InvitationSelectionController extends State<InvitationSelection> {
future: () => room.invite(id),
);
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context).contactHasBeenInvitedToTheGroup),
),
if (!context.mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.contactHasBeenInvitedToTheGroup)),
);
}
}
@ -91,6 +92,7 @@ class InvitationSelectionController extends State<InvitationSelection> {
try {
response = await matrix.client.searchUserDirectory(text, limit: 10);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text((e).toLocalizedString(context))));

View file

@ -82,6 +82,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
},
);
if (valid.error != null) {
if (!mounted) return;
await showOkAlertDialog(
useRootNavigator: false,
context: context,
@ -178,9 +179,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
);
buttons.add(
AdaptiveDialogAction(
onPressed: () => widget.request.rejectVerification().then(
(_) => Navigator.of(context, rootNavigator: false).pop(false),
),
onPressed: () => widget.request.rejectVerification().then((_) {
if (!context.mounted) return;
Navigator.of(context, rootNavigator: false).pop(false);
}),
child: Text(
L10n.of(context).reject,
style: TextStyle(color: theme.colorScheme.error),

View file

@ -130,15 +130,19 @@ class LoginController extends State<Login> {
Logs().v(
'$newDomain is not running a homeserver, asking to use $oldHomeserver',
);
if (!mounted) return;
final l10n = L10n.of(context);
final dialogResult = await showOkCancelAlertDialog(
context: context,
useRootNavigator: false,
title: L10n.of(
context,
).noMatrixServer(newDomain.toString(), oldHomeserver.toString()),
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.noMatrixServer(
newDomain.toString(),
oldHomeserver.toString(),
),
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
);
if (!mounted) return;
if (dialogResult == OkCancelResult.ok) {
if (mounted) setState(() => usernameError = null);
} else {
@ -156,26 +160,30 @@ class LoginController extends State<Login> {
}
} catch (e) {
widget.client.homeserver = oldHomeserver;
if (!mounted) return;
usernameError = e.toLocalizedString(context);
if (mounted) setState(() {});
}
}
Future<void> passwordForgotten() async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final input = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).passwordForgotten,
message: L10n.of(context).enterAnEmailAddress,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.passwordForgotten,
message: l10n.enterAnEmailAddress,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
initialText: usernameController.text.isEmail
? usernameController.text
: '',
hintText: L10n.of(context).enterAnEmailAddress,
hintText: l10n.enterAnEmailAddress,
keyboardType: TextInputType.emailAddress,
);
if (input == null) return;
if (!mounted) return;
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
final response = await showFutureLoadingDialog(
context: context,
@ -186,27 +194,30 @@ class LoginController extends State<Login> {
),
);
if (response.error != null) return;
if (!mounted) return;
final password = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).passwordForgotten,
message: L10n.of(context).chooseAStrongPassword,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.passwordForgotten,
message: l10n.chooseAStrongPassword,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
hintText: '******',
obscureText: true,
minLines: 1,
maxLines: 1,
);
if (password == null) return;
if (!mounted) return;
final ok = await showOkAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).weSentYouAnEmail,
message: L10n.of(context).pleaseClickOnLink,
okLabel: L10n.of(context).iHaveClickedOnLink,
title: l10n.weSentYouAnEmail,
message: l10n.pleaseClickOnLink,
okLabel: l10n.iHaveClickedOnLink,
);
if (ok != OkCancelResult.ok) return;
if (!mounted) return;
final data = <String, dynamic>{
'new_password': password,
'logout_devices': false,
@ -226,9 +237,10 @@ class LoginController extends State<Login> {
data: data,
),
);
if (!mounted) return;
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.passwordHasBeenChanged)),
);
usernameController.text = input;
passwordController.text = password;

View file

@ -81,17 +81,19 @@ class NewPrivateChatController extends State<NewPrivateChat> {
void inviteAction() => FluffyShare.shareInviteLink(context);
Future<void> openScannerAction() async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
if (PlatformInfos.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
if (!mounted) return;
if (info.version.sdkInt < 21) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context).unsupportedAndroidVersionLong),
),
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.unsupportedAndroidVersionLong)),
);
return;
}
}
if (!mounted) return;
await showAdaptiveBottomSheet(
context: context,
builder: (_) => QrScannerModal(
@ -101,12 +103,15 @@ class NewPrivateChatController extends State<NewPrivateChat> {
}
Future<void> copyUserId() async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context);
await Clipboard.setData(
ClipboardData(text: Matrix.of(context).client.userID!),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(L10n.of(context).copiedToClipboard)));
if (!mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.copiedToClipboard)),
);
}
void openUserModal(Profile profile) =>

View file

@ -66,6 +66,7 @@ class QrScannerModalState extends State<QrScannerModal> {
late StreamSubscription sub;
sub = controller.scannedDataStream.listen((scanData) {
sub.cancel();
if (!mounted) return;
Navigator.of(context).pop();
final data = scanData.code;
if (data != null) widget.onScan(data);

View file

@ -37,18 +37,20 @@ class SettingsController extends State<Settings> {
});
Future<void> setDisplaynameAction() async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final profile = await profileFuture;
if (!mounted) return;
final input = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).editDisplayname,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
initialText:
profile?.displayName ?? Matrix.of(context).client.userID!.localpart,
title: l10n.editDisplayname,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
initialText: profile?.displayName ?? matrix.client.userID!.localpart,
);
if (input == null) return;
final matrix = Matrix.of(context);
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => matrix.client.setProfileField(
@ -63,19 +65,19 @@ class SettingsController extends State<Settings> {
}
Future<void> logoutAction() async {
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSureYouWantToLogout,
message: L10n.of(context).noBackupWarning,
isDestructive: cryptoIdentityConnected == false,
okLabel: L10n.of(context).logout,
cancelLabel: L10n.of(context).cancel,
) ==
OkCancelResult.cancel) {
return;
}
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final consent = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: l10n.areYouSureYouWantToLogout,
message: l10n.noBackupWarning,
isDestructive: cryptoIdentityConnected == false,
okLabel: l10n.logout,
cancelLabel: l10n.cancel,
);
if (consent != OkCancelResult.ok) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => matrix.client.logout(),
@ -83,24 +85,27 @@ class SettingsController extends State<Settings> {
}
Future<void> setAvatarAction() async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final profile = await profileFuture;
if (!mounted) return;
final actions = [
if (PlatformInfos.isMobile)
AdaptiveModalAction(
value: AvatarAction.camera,
label: L10n.of(context).openCamera,
label: l10n.openCamera,
isDefaultAction: true,
icon: const Icon(Icons.camera_alt_outlined),
),
AdaptiveModalAction(
value: AvatarAction.file,
label: L10n.of(context).openGallery,
label: l10n.openGallery,
icon: const Icon(Icons.photo_outlined),
),
if (profile?.avatarUrl != null)
AdaptiveModalAction(
value: AvatarAction.remove,
label: L10n.of(context).removeYourAvatar,
label: l10n.removeYourAvatar,
isDestructive: true,
icon: const Icon(Icons.delete_outlined),
),
@ -109,12 +114,12 @@ class SettingsController extends State<Settings> {
? actions.single.value
: await showModalActionPopup<AvatarAction>(
context: context,
title: L10n.of(context).changeYourAvatar,
cancelLabel: L10n.of(context).cancel,
title: l10n.changeYourAvatar,
cancelLabel: l10n.cancel,
actions: actions,
);
if (action == null) return;
final matrix = Matrix.of(context);
if (!mounted) return;
if (action == AvatarAction.remove) {
final success = await showFutureLoadingDialog(
context: context,
@ -139,12 +144,14 @@ class SettingsController extends State<Settings> {
bytes = await result.readAsBytes();
name = result.path;
} else {
if (!mounted) return;
final result = await selectFiles(context, type: FileType.image);
final pickedFile = result.firstOrNull;
if (pickedFile == null) return;
bytes = await pickedFile.readAsBytes();
name = pickedFile.name;
}
if (!mounted) return;
final cropped = await showDialog<Uint8List>(
context: context,
builder: (contect) => AvatarCropDialog(image: bytes),
@ -155,6 +162,7 @@ class SettingsController extends State<Settings> {
bytes: bytes,
name: name,
);
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => matrix.client.setAvatar(file),
@ -181,6 +189,7 @@ class SettingsController extends State<Settings> {
}
final state = await client.getCryptoIdentityState();
if (!mounted) return;
setState(() {
cryptoIdentityConnected = state.initialized && state.connected;
});

View file

@ -19,41 +19,48 @@ class Settings3Pid extends StatefulWidget {
class Settings3PidController extends State<Settings3Pid> {
Future<void> add3PidAction() async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
final input = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).enterAnEmailAddress,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).enterAnEmailAddress,
title: l10n.enterAnEmailAddress,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
hintText: l10n.enterAnEmailAddress,
keyboardType: TextInputType.emailAddress,
);
if (input == null) return;
if (!mounted) return;
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
final response = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.requestTokenToRegisterEmail(
future: () => matrix.client.requestTokenToRegisterEmail(
clientSecret,
input,
Settings3Pid.sendAttempt++,
),
);
if (response.error != null) return;
if (!mounted) return;
final ok = await showOkAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).weSentYouAnEmail,
message: L10n.of(context).pleaseClickOnLink,
okLabel: L10n.of(context).iHaveClickedOnLink,
title: l10n.weSentYouAnEmail,
message: l10n.pleaseClickOnLink,
okLabel: l10n.iHaveClickedOnLink,
);
if (ok != OkCancelResult.ok) return;
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
delay: false,
future: () => Matrix.of(context).client.uiaRequestBackground(
(auth) => Matrix.of(
context,
).client.add3PID(clientSecret, response.result!.sid, auth: auth),
future: () => matrix.client.uiaRequestBackground(
(auth) => matrix.client.add3PID(
clientSecret,
response.result!.sid,
auth: auth,
),
),
);
if (success.error != null) return;
@ -63,21 +70,25 @@ class Settings3PidController extends State<Settings3Pid> {
Future<List<ThirdPartyIdentifier>?>? request;
Future<void> delete3Pid(ThirdPartyIdentifier identifier) async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).cancel,
title: l10n.areYouSure,
okLabel: l10n.yes,
cancelLabel: l10n.cancel,
) !=
OkCancelResult.ok) {
return;
}
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(
context,
).client.delete3pidFromAccount(identifier.address, identifier.medium),
future: () => matrix.client.delete3pidFromAccount(
identifier.address,
identifier.medium,
),
);
if (success.error != null) return;
setState(() => request = null);

View file

@ -91,6 +91,7 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
}
Future<void> _addEmotePack() async {
final matrix = Matrix.of(context);
setState(() {
_loading = true;
_progress = 0;
@ -148,7 +149,7 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
} else {
mxcFile = thumbnail;
}
final uri = await Matrix.of(context).client.uploadContent(
final uri = await matrix.client.uploadContent(
mxcFile.bytes,
filename: mxcFile.name,
contentType: mxcFile.mimeType,
@ -178,6 +179,7 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
}
}
if (!mounted) return;
await widget.controller.save(context);
_importMap.removeWhere(
(key, value) => successfulUploads.contains(key.name),

View file

@ -293,6 +293,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
}
Future<void> createStickers() async {
final matrix = Matrix.of(context);
final pickedFiles = await selectFiles(
context,
type: FileType.image,
@ -315,7 +316,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
nativeImplementations: ClientManager.nativeImplementations,
) ??
file;
final uri = await Matrix.of(context).client.uploadContent(
final uri = await matrix.client.uploadContent(
file.bytes,
filename: file.name,
contentType: file.mimeType,
@ -361,6 +362,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
final buffer = InputMemoryStream(await result.single.readAsBytes());
final archive = ZipDecoder().decodeStream(buffer);
if (!mounted) return;
await showDialog(
context: context,
@ -375,7 +377,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
Future<void> exportAsZip() async {
final client = Matrix.of(context).client;
await showFutureLoadingDialog(
final result = await showFutureLoadingDialog<MatrixFile>(
context: context,
future: () async {
final pack = _getPack();
@ -397,11 +399,12 @@ class EmotesSettingsController extends State<EmotesSettings> {
'${pack.pack.displayName ?? client.userID?.localpart ?? 'emotes'}.zip';
final output = ZipEncoder().encode(archive);
MatrixFile(
name: fileName,
bytes: Uint8List.fromList(output),
).save(context);
return MatrixFile(name: fileName, bytes: Uint8List.fromList(output));
},
);
final file = result.result;
if (file == null) return;
if (!mounted) return;
file.save(context);
}
}

View file

@ -40,6 +40,7 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
],
);
if (delete != true) return;
if (!mounted) return;
final success = await showFutureLoadingDialog(
context: context,

View file

@ -1,7 +1,10 @@
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/settings_notifications/push_rule_extensions.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/settings_switch_list_tile.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
@ -47,6 +50,11 @@ class SettingsNotificationsView extends StatelessWidget {
return SelectionArea(
child: Column(
children: [
if (kIsWeb)
SettingsSwitchListTile.adaptive(
title: L10n.of(context).playSoundOnNotification,
setting: AppSettings.webNotificationSound,
),
if (pushRules != null)
for (final category in pushCategories) ...[
ListTile(

View file

@ -24,6 +24,8 @@ class SettingsPasswordController extends State<SettingsPassword> {
bool loading = false;
Future<void> changePassword() async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
setState(() {
oldPasswordError = newPassword1Error = newPassword2Error = null;
});
@ -51,13 +53,13 @@ class SettingsPasswordController extends State<SettingsPassword> {
loading = true;
});
try {
final scaffoldMessenger = ScaffoldMessenger.of(context);
await Matrix.of(context).client.changePassword(
newPassword1Controller.text,
oldPassword: oldPasswordController.text,
);
if (!mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)),
SnackBar(content: Text(l10n.passwordHasBeenChanged)),
);
if (mounted) context.pop();
} catch (e) {

View file

@ -19,20 +19,21 @@ class SettingsSecurity extends StatefulWidget {
class SettingsSecurityController extends State<SettingsSecurity> {
Future<void> setAppLockAction() async {
final l10n = L10n.of(context);
if (AppLock.of(context).isActive) {
AppLock.of(context).showLockScreen();
}
final newLock = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).pleaseChooseAPasscode,
message: L10n.of(context).pleaseEnter4Digits,
cancelLabel: L10n.of(context).cancel,
title: l10n.pleaseChooseAPasscode,
message: l10n.pleaseEnter4Digits,
cancelLabel: l10n.cancel,
validator: (text) {
if (text.isEmpty || (text.length == 4 && int.tryParse(text)! >= 0)) {
return null;
}
return L10n.of(context).pleaseEnter4Digits;
return l10n.pleaseEnter4Digits;
},
keyboardType: TextInputType.number,
obscureText: true,
@ -41,53 +42,55 @@ class SettingsSecurityController extends State<SettingsSecurity> {
maxLength: 4,
);
if (newLock != null) {
if (!mounted) return;
await AppLock.of(context).changePincode(newLock);
}
}
Future<void> deleteAccountAction() async {
final l10n = L10n.of(context);
final matrix = Matrix.of(context);
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).warning,
message: L10n.of(context).deactivateAccountWarning,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
title: l10n.warning,
message: l10n.deactivateAccountWarning,
okLabel: l10n.ok,
cancelLabel: l10n.cancel,
isDestructive: true,
) ==
OkCancelResult.cancel) {
return;
}
final supposedMxid = Matrix.of(context).client.userID!;
if (!mounted) return;
final supposedMxid = matrix.client.userID!;
final mxid = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).confirmMatrixId,
validator: (text) => text == supposedMxid
? null
: L10n.of(context).supposedMxid(supposedMxid),
title: l10n.confirmMatrixId,
validator: (text) =>
text == supposedMxid ? null : l10n.supposedMxid(supposedMxid),
isDestructive: true,
okLabel: L10n.of(context).delete,
cancelLabel: L10n.of(context).cancel,
okLabel: l10n.delete,
cancelLabel: l10n.cancel,
);
if (mxid == null || mxid.isEmpty || mxid != supposedMxid) {
return;
}
if (!mounted) return;
final resp = await showFutureLoadingDialog(
context: context,
delay: false,
future: () =>
Matrix.of(context).client.uiaRequestBackground<IdServerUnbindResult?>(
(auth) => Matrix.of(
context,
).client.deactivateAccount(auth: auth, erase: true),
),
future: () => matrix.client.uiaRequestBackground<IdServerUnbindResult?>(
(auth) => matrix.client.deactivateAccount(auth: auth, erase: true),
),
);
if (!resp.isError) {
if (!mounted) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.logout(),
future: () => matrix.client.logout(),
);
}
}

View file

@ -29,6 +29,7 @@ class SettingsStyleController extends State<SettingsStyle> {
final picked = await selectFiles(context, type: FileType.image);
final pickedFile = picked.firstOrNull;
if (pickedFile == null) return;
if (!mounted) return;
await showFutureLoadingDialog(
context: context,

View file

@ -108,6 +108,7 @@ class SignInPage extends StatelessWidget {
SizedBox.square(
dimension: 32,
child: IconButton(
tooltip: website,
icon: const Icon(
Icons.open_in_new_outlined,
size: 16,