fluffychat merge - resolve conflicts

pull/1384/head
ggurdin 1 year ago
commit c58226807a
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -2789,5 +2789,9 @@
} }
}, },
"changelog": "Änderungsprotokoll", "changelog": "Änderungsprotokoll",
"@changelog": {} "@changelog": {},
"sendCanceled": "Senden abgebrochen",
"@sendCanceled": {},
"noChatsFoundHere": "Hier wurden noch keine Chats gefunden. Starten Sie einen neuen Chat mit jemandem, indem Sie die Schaltfläche unten verwenden. ⤵️",
"@noChatsFoundHere": {}
} }

@ -4194,5 +4194,6 @@
"loginWithMatrixId": "Login with Matrix-ID", "loginWithMatrixId": "Login with Matrix-ID",
"discoverHomeservers": "Discover homeservers", "discoverHomeservers": "Discover homeservers",
"whatIsAHomeserver": "What is a homeserver?", "whatIsAHomeserver": "What is a homeserver?",
"homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org." "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.",
"doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?"
} }

@ -342,7 +342,7 @@ class ChatController extends State<ChatPageWithRoom>
void tryLoadTimeline() async { void tryLoadTimeline() async {
final initialEventId = widget.eventId; final initialEventId = widget.eventId;
loadTimelineFuture = getTimeline(); loadTimelineFuture = _getTimeline();
try { try {
await loadTimelineFuture; await loadTimelineFuture;
if (initialEventId != null) scrollToEventId(initialEventId); if (initialEventId != null) scrollToEventId(initialEventId);
@ -394,6 +394,7 @@ class ChatController extends State<ChatPageWithRoom>
void updateView() { void updateView() {
if (!mounted) return; if (!mounted) return;
setReadMarker();
setState(() {}); setState(() {});
} }
@ -402,18 +403,10 @@ class ChatController extends State<ChatPageWithRoom>
int? animateInEventIndex; int? animateInEventIndex;
void onInsert(int i) { void onInsert(int i) {
onChange(i);
// setState will be called by updateView() anyway // setState will be called by updateView() anyway
animateInEventIndex = i; animateInEventIndex = i;
} }
void onChange(int i) {
if (timeline?.events[i].status == EventStatus.synced) {
final index = timeline!.events.firstIndexWhereNotError;
if (i == index) setReadMarker(eventId: timeline?.events[i].eventId);
}
}
// #Pangea // #Pangea
List<Event> get visibleEvents => List<Event> get visibleEvents =>
timeline?.events timeline?.events
@ -424,7 +417,7 @@ class ChatController extends State<ChatPageWithRoom>
<Event>[]; <Event>[];
// Pangea# // Pangea#
Future<void> getTimeline({ Future<void> _getTimeline({
String? eventContextId, String? eventContextId,
}) async { }) async {
await Matrix.of(context).client.roomsLoading; await Matrix.of(context).client.roomsLoading;
@ -439,7 +432,6 @@ class ChatController extends State<ChatPageWithRoom>
onUpdate: updateView, onUpdate: updateView,
eventContextId: eventContextId, eventContextId: eventContextId,
onInsert: onInsert, onInsert: onInsert,
onChange: onChange,
); );
// #Pangea // #Pangea
if (visibleEvents.length < 10 && timeline != null) { if (visibleEvents.length < 10 && timeline != null) {
@ -461,7 +453,6 @@ class ChatController extends State<ChatPageWithRoom>
timeline = await room.getTimeline( timeline = await room.getTimeline(
onUpdate: updateView, onUpdate: updateView,
onInsert: onInsert, onInsert: onInsert,
onChange: onChange,
); );
if (!mounted) return; if (!mounted) return;
if (e is TimeoutException || e is IOException) { if (e is TimeoutException || e is IOException) {
@ -580,7 +571,7 @@ class ChatController extends State<ChatPageWithRoom>
} }
// then cancel the old timeline // then cancel the old timeline
// fixes bug with read reciepts and quick switching // fixes bug with read reciepts and quick switching
loadTimelineFuture = getTimeline(eventContextId: room.fullyRead).onError( loadTimelineFuture = _getTimeline(eventContextId: room.fullyRead).onError(
ErrorReporter( ErrorReporter(
context, context,
'Unable to load timeline after changing sending Client', 'Unable to load timeline after changing sending Client',
@ -1175,7 +1166,7 @@ class ChatController extends State<ChatPageWithRoom>
setState(() { setState(() {
timeline = null; timeline = null;
_scrolledUp = false; _scrolledUp = false;
loadTimelineFuture = getTimeline(eventContextId: eventId).onError( loadTimelineFuture = _getTimeline(eventContextId: eventId).onError(
ErrorReporter(context, 'Unable to load timeline after scroll to ID') ErrorReporter(context, 'Unable to load timeline after scroll to ID')
.onErrorCallback, .onErrorCallback,
); );
@ -1204,7 +1195,7 @@ class ChatController extends State<ChatPageWithRoom>
setState(() { setState(() {
timeline = null; timeline = null;
_scrolledUp = false; _scrolledUp = false;
loadTimelineFuture = getTimeline().onError( loadTimelineFuture = _getTimeline().onError(
ErrorReporter(context, 'Unable to load timeline after scroll down') ErrorReporter(context, 'Unable to load timeline after scroll down')
.onErrorCallback, .onErrorCallback,
); );
@ -1717,12 +1708,3 @@ class ChatController extends State<ChatPageWithRoom>
} }
enum EmojiPickerType { reaction, keyboard } enum EmojiPickerType { reaction, keyboard }
extension on List<Event> {
int get firstIndexWhereNotError {
if (isEmpty) return 0;
final index = indexWhere((event) => !event.status.isError);
if (index == -1) return length;
return index;
}
}

@ -34,6 +34,7 @@ class ChatMembersView extends StatelessWidget {
(room.summary.mInvitedMemberCount ?? 0); (room.summary.mInvitedMemberCount ?? 0);
final error = controller.error; final error = controller.error;
final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -90,6 +91,16 @@ class ChatMembersView extends StatelessWidget {
controller: controller.filterController, controller: controller.filterController,
onChanged: controller.setFilter, onChanged: controller.setFilter,
decoration: InputDecoration( decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
prefixIcon: const Icon(Icons.search_outlined), prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context)!.search, hintText: L10n.of(context)!.search,
), ),

@ -31,6 +31,8 @@ class ChatSearchView extends StatelessWidget {
); );
} }
final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: const Center(child: BackButton()), leading: const Center(child: BackButton()),
@ -59,6 +61,16 @@ class ChatSearchView extends StatelessWidget {
decoration: InputDecoration( decoration: InputDecoration(
hintText: L10n.of(context)!.search, hintText: L10n.of(context)!.search,
suffixIcon: const Icon(Icons.search_outlined), suffixIcon: const Icon(Icons.search_outlined),
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
), ),
), ),
), ),

@ -86,7 +86,17 @@ class HomeserverPickerController extends State<HomeserverPicker> {
Future<void> checkHomeserverAction([_]) async { Future<void> checkHomeserverAction([_]) async {
homeserverController.text = homeserverController.text =
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
if (homeserverController.text == _lastCheckedUrl) return;
if (homeserverController.text.isEmpty) {
setState(() {
error = loginFlows = null;
isLoading = false;
Matrix.of(context).getLoginClient().homeserver = null;
});
return;
}
if (_lastCheckedUrl == homeserverController.text) return;
_lastCheckedUrl = homeserverController.text; _lastCheckedUrl = homeserverController.text;
setState(() { setState(() {
error = loginFlows = null; error = loginFlows = null;
@ -102,7 +112,12 @@ class HomeserverPickerController extends State<HomeserverPicker> {
final (_, _, loginFlows) = await client.checkHomeserver(homeserver); final (_, _, loginFlows) = await client.checkHomeserver(homeserver);
this.loginFlows = loginFlows; this.loginFlows = loginFlows;
} catch (e) { } catch (e) {
setState(() => error = (e).toLocalizedString(context)); setState(
() => error = (e).toLocalizedString(
context,
ExceptionContext.checkHomeserver,
),
);
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => isLoading = false); setState(() => isLoading = false);

@ -32,7 +32,8 @@ class HomeserverPickerView extends StatelessWidget {
), ),
), ),
// Pangea# // Pangea#
body: ListView( body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// display a prominent banner to import session for TOR browser // display a prominent banner to import session for TOR browser
// users. This feature is just some UX sugar as TOR users are // users. This feature is just some UX sugar as TOR users are
@ -58,31 +59,29 @@ class HomeserverPickerView extends StatelessWidget {
// ), // ),
// ), // ),
// ), // ),
// Image.asset( // if (MediaQuery.of(context).size.height > 512)
// ConstrainedBox(
// constraints: BoxConstraints(
// maxHeight: MediaQuery.of(context).size.height / 4,
// ),
// child: Image.asset(
// 'assets/banner_transparent.png', // 'assets/banner_transparent.png',
// alignment: Alignment.center,
// repeat: ImageRepeat.repeat,
// ),
// ), // ),
// Pangea# // Pangea#
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.all(32.0),
top: 16.0,
right: 8.0,
left: 8.0,
bottom: 16.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField( child: TextField(
onChanged: controller.tryCheckHomeserverActionWithCooldown, onChanged: controller.tryCheckHomeserverActionWithCooldown,
onEditingComplete: onEditingComplete:
controller.tryCheckHomeserverActionWithoutCooldown, controller.tryCheckHomeserverActionWithoutCooldown,
onSubmitted: onSubmitted: controller.tryCheckHomeserverActionWithoutCooldown,
controller.tryCheckHomeserverActionWithoutCooldown,
onTap: controller.tryCheckHomeserverActionWithCooldown, onTap: controller.tryCheckHomeserverActionWithCooldown,
controller: controller.homeserverController, controller: controller.homeserverController,
autocorrect: false,
keyboardType: TextInputType.url,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: controller.isLoading prefixIcon: controller.isLoading
? Container( ? Container(
@ -100,8 +99,7 @@ class HomeserverPickerView extends StatelessWidget {
: const Icon(Icons.search_outlined), : const Icon(Icons.search_outlined),
filled: false, filled: false,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: borderRadius: BorderRadius.circular(AppConfig.borderRadius),
BorderRadius.circular(AppConfig.borderRadius),
), ),
hintText: AppConfig.defaultHomeserver, hintText: AppConfig.defaultHomeserver,
labelText: L10n.of(context)!.homeserver, labelText: L10n.of(context)!.homeserver,
@ -137,29 +135,26 @@ class HomeserverPickerView extends StatelessWidget {
), ),
), ),
), ),
if (controller.supportsPasswordLogin || controller.supportsSso) if (MediaQuery.of(context).size.height > 512) const Spacer(),
Padding( ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 32.0,
vertical: 8.0, vertical: 32.0,
), ),
child: ElevatedButton( children: [
style: ElevatedButton.styleFrom( TextButton(
backgroundColor: theme.colorScheme.primary, style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onPrimary, textStyle: theme.textTheme.labelMedium,
foregroundColor: theme.colorScheme.secondary,
), ),
onPressed: controller.isLoggingIn || controller.isLoading onPressed: controller.isLoggingIn || controller.isLoading
? null ? null
: controller.supportsSso : controller.restoreBackup,
? controller.ssoLoginAction child: Text(L10n.of(context)!.hydrate),
: controller.login,
child: Text(L10n.of(context)!.connect),
),
), ),
if (controller.supportsPasswordLogin && controller.supportsSso) if (controller.supportsPasswordLogin && controller.supportsSso)
Padding( TextButton(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.secondary, foregroundColor: theme.colorScheme.secondary,
textStyle: theme.textTheme.labelMedium, textStyle: theme.textTheme.labelMedium,
@ -169,23 +164,22 @@ class HomeserverPickerView extends StatelessWidget {
: controller.login, : controller.login,
child: Text(L10n.of(context)!.loginWithMatrixId), child: Text(L10n.of(context)!.loginWithMatrixId),
), ),
), const SizedBox(height: 8.0),
Padding( if (controller.supportsPasswordLogin || controller.supportsSso)
padding: const EdgeInsets.symmetric(horizontal: 16.0), ElevatedButton(
child: TextButton( style: ElevatedButton.styleFrom(
style: TextButton.styleFrom( backgroundColor: theme.colorScheme.primary,
textStyle: theme.textTheme.labelMedium, foregroundColor: theme.colorScheme.onPrimary,
foregroundColor: theme.colorScheme.secondary,
), ),
onPressed: controller.isLoggingIn || controller.isLoading onPressed: controller.isLoggingIn || controller.isLoading
? null ? null
: controller.restoreBackup, : controller.supportsSso
child: Text(L10n.of(context)!.hydrate), ? controller.ssoLoginAction
), : controller.login,
child: Text(L10n.of(context)!.next),
), ),
], ],
), ),
),
], ],
), ),
); );

@ -30,6 +30,7 @@ class InvitationSelectionView extends StatelessWidget {
// #Pangea // #Pangea
// final groupName = room.name.isEmpty ? L10n.of(context)!.group : room.name; // final groupName = room.name.isEmpty ? L10n.of(context)!.group : room.name;
// Pangea# // Pangea#
final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// #Pangea // #Pangea
@ -52,6 +53,16 @@ class InvitationSelectionView extends StatelessWidget {
child: TextField( child: TextField(
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
decoration: InputDecoration( decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
// #Pangea // #Pangea
hintText: controller.mode == InvitationSelectionMode.admin hintText: controller.mode == InvitationSelectionMode.admin
? L10n.of(context)!.inviteUsersFromPangea ? L10n.of(context)!.inviteUsersFromPangea

@ -74,7 +74,7 @@ class NewGroupView extends StatelessWidget {
readOnly: controller.loading, readOnly: controller.loading,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.people_outlined), prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.groupName, labelText: L10n.of(context)!.groupName,
), ),
), ),
), ),

@ -52,6 +52,16 @@ class NewPrivateChatView extends StatelessWidget {
onChanged: controller.searchUsers, onChanged: controller.searchUsers,
decoration: InputDecoration( decoration: InputDecoration(
hintText: L10n.of(context)!.searchForUsers, hintText: L10n.of(context)!.searchForUsers,
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
prefixIcon: searchResponse == null prefixIcon: searchResponse == null
? const Icon(Icons.search_outlined) ? const Icon(Icons.search_outlined)
: FutureBuilder( : FutureBuilder(

@ -61,7 +61,7 @@ class NewSpaceView extends StatelessWidget {
readOnly: controller.loading, readOnly: controller.loading,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.people_outlined), prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.spaceName, labelText: L10n.of(context)!.spaceName,
// #Pangea // #Pangea
// errorText: controller.nameError, // errorText: controller.nameError,
// Pangea# // Pangea#

@ -45,7 +45,7 @@ class SettingsIgnoreListView extends StatelessWidget {
labelText: L10n.of(context)!.blockUsername, labelText: L10n.of(context)!.blockUsername,
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: L10n.of(context)!.block, tooltip: L10n.of(context)!.block,
icon: const Icon(Icons.send_outlined), icon: const Icon(Icons.add),
onPressed: () => controller.ignoreUser(context), onPressed: () => controller.ignoreUser(context),
), ),
), ),

@ -17,12 +17,6 @@ class SettingsPasswordView extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context)!.changePassword), title: Text(L10n.of(context)!.changePassword),
actions: [
TextButton(
child: Text(L10n.of(context)!.passwordRecoverySettings),
onPressed: () => context.go('/rooms/settings/security/3pid'),
),
],
), ),
body: ListTileTheme( body: ListTileTheme(
iconColor: theme.colorScheme.onSurface, iconColor: theme.colorScheme.onSurface,
@ -31,13 +25,6 @@ class SettingsPasswordView extends StatelessWidget {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
Center(
child: Icon(
Icons.key_outlined,
color: theme.dividerColor,
size: 80,
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: controller.oldPasswordController, controller: controller.oldPasswordController,
@ -46,18 +33,22 @@ class SettingsPasswordView extends StatelessWidget {
autofocus: true, autofocus: true,
readOnly: controller.loading, readOnly: controller.loading,
decoration: InputDecoration( decoration: InputDecoration(
hintText: L10n.of(context)!.pleaseEnterYourCurrentPassword, prefixIcon: const Icon(Icons.lock_outlined),
hintText: '********',
labelText: L10n.of(context)!.pleaseEnterYourCurrentPassword,
errorText: controller.oldPasswordError, errorText: controller.oldPasswordError,
), ),
), ),
const Divider(height: 32), const Divider(height: 64),
TextField( TextField(
controller: controller.newPassword1Controller, controller: controller.newPassword1Controller,
obscureText: true, obscureText: true,
autocorrect: false, autocorrect: false,
readOnly: controller.loading, readOnly: controller.loading,
decoration: InputDecoration( decoration: InputDecoration(
hintText: L10n.of(context)!.newPassword, prefixIcon: const Icon(Icons.lock_reset_outlined),
hintText: '********',
labelText: L10n.of(context)!.newPassword,
errorText: controller.newPassword1Error, errorText: controller.newPassword1Error,
), ),
), ),
@ -68,22 +59,28 @@ class SettingsPasswordView extends StatelessWidget {
autocorrect: false, autocorrect: false,
readOnly: controller.loading, readOnly: controller.loading,
decoration: InputDecoration( decoration: InputDecoration(
hintText: L10n.of(context)!.repeatPassword, prefixIcon: const Icon(Icons.repeat_outlined),
hintText: '********',
labelText: L10n.of(context)!.repeatPassword,
errorText: controller.newPassword2Error, errorText: controller.newPassword2Error,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 32),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton(
onPressed: onPressed:
controller.loading ? null : controller.changePassword, controller.loading ? null : controller.changePassword,
icon: const Icon(Icons.send_outlined), child: controller.loading
label: controller.loading
? const LinearProgressIndicator() ? const LinearProgressIndicator()
: Text(L10n.of(context)!.changePassword), : Text(L10n.of(context)!.changePassword),
), ),
), ),
const SizedBox(height: 16),
TextButton(
child: Text(L10n.of(context)!.passwordRecoverySettings),
onPressed: () => context.go('/rooms/settings/security/3pid'),
),
], ],
), ),
), ),

@ -86,9 +86,7 @@ class SettingsSecurityView extends StatelessWidget {
onTap: controller.setAppLockAction, onTap: controller.setAppLockAction,
), ),
}, },
Divider( Divider(color: theme.dividerColor),
color: theme.dividerColor,
),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context)!.account, L10n.of(context)!.account,
@ -117,13 +115,14 @@ class SettingsSecurityView extends StatelessWidget {
), ),
ListTile( ListTile(
iconColor: Colors.orange, iconColor: Colors.orange,
leading: const Icon(Icons.tap_and_play), leading: const Icon(Icons.delete_sweep_outlined),
title: Text( title: Text(
L10n.of(context)!.dehydrate, L10n.of(context)!.dehydrate,
style: const TextStyle(color: Colors.orange), style: const TextStyle(color: Colors.orange),
), ),
onTap: controller.dehydrateAction, onTap: controller.dehydrateAction,
), ),
Divider(color: theme.dividerColor),
ListTile( ListTile(
iconColor: Colors.red, iconColor: Colors.red,
leading: const Icon(Icons.delete_outlined), leading: const Icon(Icons.delete_outlined),

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:http/http.dart';
import 'package:matrix/encryption.dart'; import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -68,9 +69,14 @@ extension LocalizedExceptionExtension on Object {
} }
if (this is IOException || if (this is IOException ||
this is SocketException || this is SocketException ||
this is SyncConnectionException) { this is SyncConnectionException ||
this is ClientException) {
return L10n.of(context)!.noConnectionToTheServer; return L10n.of(context)!.noConnectionToTheServer;
} }
if (this is FormatException &&
exceptionContext == ExceptionContext.checkHomeserver) {
return L10n.of(context)!.doesNotSeemToBeAValidHomeserver;
}
if (this is String) return toString(); if (this is String) return toString();
if (this is UiaException) return toString(); if (this is UiaException) return toString();
Logs().w('Something went wrong: ', this); Logs().w('Something went wrong: ', this);
@ -80,4 +86,5 @@ extension LocalizedExceptionExtension on Object {
enum ExceptionContext { enum ExceptionContext {
changePassword, changePassword,
checkHomeserver,
} }

Loading…
Cancel
Save