mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-12-24 06:22:34 +01:00
refactor: Added and applied require_trailing_commas linter rule
This commit is contained in:
parent
ed9e58d0bf
commit
5212d7ce4d
@ -8,6 +8,7 @@ linter:
|
|||||||
- prefer_final_locals
|
- prefer_final_locals
|
||||||
- prefer_final_in_for_each
|
- prefer_final_in_for_each
|
||||||
- sort_pub_dependencies
|
- sort_pub_dependencies
|
||||||
|
- require_trailing_commas
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
|
@ -144,13 +144,15 @@ void main() {
|
|||||||
|
|
||||||
await tester.waitFor(
|
await tester.waitFor(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(InvitationSelectionView),
|
of: find.byType(InvitationSelectionView),
|
||||||
matching: find.byType(TextField)),
|
matching: find.byType(TextField),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(InvitationSelectionView),
|
of: find.byType(InvitationSelectionView),
|
||||||
matching: find.byType(TextField)),
|
matching: find.byType(TextField),
|
||||||
|
),
|
||||||
Users.user2.name,
|
Users.user2.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -160,14 +162,17 @@ void main() {
|
|||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find
|
await tester.tap(
|
||||||
.descendant(
|
find
|
||||||
|
.descendant(
|
||||||
of: find.descendant(
|
of: find.descendant(
|
||||||
of: find.byType(InvitationSelectionView),
|
of: find.byType(InvitationSelectionView),
|
||||||
matching: find.byType(ListTile),
|
matching: find.byType(ListTile),
|
||||||
),
|
),
|
||||||
matching: find.text(Users.user2.name))
|
matching: find.text(Users.user2.name),
|
||||||
.last);
|
)
|
||||||
|
.last,
|
||||||
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.waitFor(find.maybeUppercaseText('Yes'));
|
await tester.waitFor(find.maybeUppercaseText('Yes'));
|
||||||
|
@ -144,7 +144,8 @@ extension DefaultFlowExtensions on WidgetTester {
|
|||||||
do {
|
do {
|
||||||
if (DateTime.now().isAfter(end)) {
|
if (DateTime.now().isAfter(end)) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Timed out waiting for HomeserverPicker or ChatListViewBody');
|
'Timed out waiting for HomeserverPicker or ChatListViewBody',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
|
@ -70,8 +70,9 @@ abstract class AppConfig {
|
|||||||
colorSchemeSeed = Color(json['chat_color']);
|
colorSchemeSeed = Color(json['chat_color']);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
|
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
|
||||||
e);
|
e,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (json['application_name'] is String) {
|
if (json['application_name'] is String) {
|
||||||
|
@ -70,21 +70,25 @@ class AppRoutes {
|
|||||||
widget: const ChatDetails(),
|
widget: const ChatDetails(),
|
||||||
stackedRoutes: _chatDetailsRoutes,
|
stackedRoutes: _chatDetailsRoutes,
|
||||||
),
|
),
|
||||||
VWidget(path: ':roomid', widget: const Chat(), stackedRoutes: [
|
VWidget(
|
||||||
VWidget(
|
path: ':roomid',
|
||||||
path: 'encryption',
|
widget: const Chat(),
|
||||||
widget: const ChatEncryptionSettings(),
|
stackedRoutes: [
|
||||||
),
|
VWidget(
|
||||||
VWidget(
|
path: 'encryption',
|
||||||
path: 'invite',
|
widget: const ChatEncryptionSettings(),
|
||||||
widget: const InvitationSelection(),
|
),
|
||||||
),
|
VWidget(
|
||||||
VWidget(
|
path: 'invite',
|
||||||
path: 'details',
|
widget: const InvitationSelection(),
|
||||||
widget: const ChatDetails(),
|
),
|
||||||
stackedRoutes: _chatDetailsRoutes,
|
VWidget(
|
||||||
),
|
path: 'details',
|
||||||
]),
|
widget: const ChatDetails(),
|
||||||
|
stackedRoutes: _chatDetailsRoutes,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
widget: const Settings(),
|
widget: const Settings(),
|
||||||
@ -263,21 +267,22 @@ class AppRoutes {
|
|||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'connect',
|
path: 'connect',
|
||||||
widget: const ConnectPage(),
|
widget: const ConnectPage(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
stackedRoutes: [
|
stackedRoutes: [
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'login',
|
path: 'login',
|
||||||
widget: const Login(),
|
widget: const Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
widget: const SignupPage(),
|
widget: const SignupPage(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
widget: const LogViewer(),
|
widget: const LogViewer(),
|
||||||
@ -354,21 +359,22 @@ class AppRoutes {
|
|||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'connect',
|
path: 'connect',
|
||||||
widget: const ConnectPage(),
|
widget: const ConnectPage(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
stackedRoutes: [
|
stackedRoutes: [
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'login',
|
path: 'login',
|
||||||
widget: const Login(),
|
widget: const Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
widget: const SignupPage(),
|
widget: const SignupPage(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
|
@ -88,14 +88,15 @@ class AddStoryController extends State<AddStoryPage> {
|
|||||||
);
|
);
|
||||||
if (picked == null) return;
|
if (picked == null) return;
|
||||||
final matrixFile = await showFutureLoadingDialog(
|
final matrixFile = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
final bytes = await picked.readAsBytes();
|
final bytes = await picked.readAsBytes();
|
||||||
return MatrixImageFile(
|
return MatrixImageFile(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
name: picked.name,
|
name: picked.name,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
image = matrixFile.result;
|
image = matrixFile.result;
|
||||||
|
@ -92,34 +92,39 @@ class InviteStoryPageState extends State<InviteStoryPage> {
|
|||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FutureBuilder<List<User>>(
|
child: FutureBuilder<List<User>>(
|
||||||
future: loadContacts,
|
future: loadContacts,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final contacts = snapshot.data;
|
final contacts = snapshot.data;
|
||||||
if (contacts == null) {
|
if (contacts == null) {
|
||||||
final error = snapshot.error;
|
final error = snapshot.error;
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(error.toLocalizedString(context)));
|
child: Text(error.toLocalizedString(context)),
|
||||||
}
|
);
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive());
|
|
||||||
}
|
}
|
||||||
_undecided = contacts.map((u) => u.id).toSet();
|
return const Center(
|
||||||
return ListView.builder(
|
child: CircularProgressIndicator.adaptive(),
|
||||||
itemCount: contacts.length,
|
|
||||||
itemBuilder: (context, i) => SwitchListTile.adaptive(
|
|
||||||
value: _invite.contains(contacts[i].id),
|
|
||||||
onChanged: (b) => setState(() => b
|
|
||||||
? _invite.add(contacts[i].id)
|
|
||||||
: _invite.remove(contacts[i].id)),
|
|
||||||
secondary: Avatar(
|
|
||||||
mxContent: contacts[i].avatarUrl,
|
|
||||||
name: contacts[i].calcDisplayname(),
|
|
||||||
),
|
|
||||||
title: Text(contacts[i].calcDisplayname()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
_undecided = contacts.map((u) => u.id).toSet();
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: contacts.length,
|
||||||
|
itemBuilder: (context, i) => SwitchListTile.adaptive(
|
||||||
|
value: _invite.contains(contacts[i].id),
|
||||||
|
onChanged: (b) => setState(
|
||||||
|
() => b
|
||||||
|
? _invite.add(contacts[i].id)
|
||||||
|
: _invite.remove(contacts[i].id),
|
||||||
|
),
|
||||||
|
secondary: Avatar(
|
||||||
|
mxContent: contacts[i].avatarUrl,
|
||||||
|
name: contacts[i].calcDisplayname(),
|
||||||
|
),
|
||||||
|
title: Text(contacts[i].calcDisplayname()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -36,19 +36,22 @@ class ArchiveView extends StatelessWidget {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
L10n.of(context)!.oopsSomethingWentWrong,
|
L10n.of(context)!.oopsSomethingWentWrong,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
archive = snapshot.data;
|
archive = snapshot.data;
|
||||||
if (archive == null || archive!.isEmpty) {
|
if (archive == null || archive!.isEmpty) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Icon(Icons.archive_outlined, size: 80));
|
child: Icon(Icons.archive_outlined, size: 80),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: archive!.length,
|
itemCount: archive!.length,
|
||||||
|
@ -246,7 +246,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
body: Center(
|
body: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: FluffyThemes.columnWidth * 1.5),
|
maxWidth: FluffyThemes.columnWidth * 1.5,
|
||||||
|
),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
children: [
|
||||||
@ -258,7 +259,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
L10n.of(context)!.pleaseEnterRecoveryKeyDescription),
|
L10n.of(context)!.pleaseEnterRecoveryKeyDescription,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
TextField(
|
TextField(
|
||||||
@ -274,64 +276,68 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: const EdgeInsets.all(16),
|
contentPadding: const EdgeInsets.all(16),
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontFamily: Theme.of(context)
|
fontFamily:
|
||||||
.textTheme
|
Theme.of(context).textTheme.bodyLarge?.fontFamily,
|
||||||
.bodyLarge
|
),
|
||||||
?.fontFamily),
|
|
||||||
hintText: L10n.of(context)!.recoveryKey,
|
hintText: L10n.of(context)!.recoveryKey,
|
||||||
errorText: _recoveryKeyInputError,
|
errorText: _recoveryKeyInputError,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
foregroundColor:
|
foregroundColor:
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
),
|
|
||||||
icon: _recoveryKeyInputLoading
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: const Icon(Icons.lock_open_outlined),
|
|
||||||
label: Text(L10n.of(context)!.unlockOldMessages),
|
|
||||||
onPressed: _recoveryKeyInputLoading
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
setState(() {
|
|
||||||
_recoveryKeyInputError = null;
|
|
||||||
_recoveryKeyInputLoading = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
final key =
|
|
||||||
_recoveryKeyTextEditingController.text;
|
|
||||||
await bootstrap.newSsssKey!.unlock(
|
|
||||||
keyOrPassphrase: key,
|
|
||||||
);
|
|
||||||
Logs().d('SSSS unlocked');
|
|
||||||
await bootstrap
|
|
||||||
.client.encryption!.crossSigning
|
|
||||||
.selfSign(
|
|
||||||
keyOrPassphrase: key,
|
|
||||||
);
|
|
||||||
Logs().d('Successful elfsigned');
|
|
||||||
await bootstrap.openExistingSsss();
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().w('Unable to unlock SSSS', e, s);
|
|
||||||
setState(() => _recoveryKeyInputError =
|
|
||||||
L10n.of(context)!.oopsSomethingWentWrong);
|
|
||||||
} finally {
|
|
||||||
setState(
|
|
||||||
() => _recoveryKeyInputLoading = false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(children: [
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Text(L10n.of(context)!.or),
|
|
||||||
),
|
),
|
||||||
const Expanded(child: Divider()),
|
icon: _recoveryKeyInputLoading
|
||||||
]),
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.lock_open_outlined),
|
||||||
|
label: Text(L10n.of(context)!.unlockOldMessages),
|
||||||
|
onPressed: _recoveryKeyInputLoading
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
setState(() {
|
||||||
|
_recoveryKeyInputError = null;
|
||||||
|
_recoveryKeyInputLoading = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final key =
|
||||||
|
_recoveryKeyTextEditingController.text;
|
||||||
|
await bootstrap.newSsssKey!.unlock(
|
||||||
|
keyOrPassphrase: key,
|
||||||
|
);
|
||||||
|
Logs().d('SSSS unlocked');
|
||||||
|
await bootstrap.client.encryption!.crossSigning
|
||||||
|
.selfSign(
|
||||||
|
keyOrPassphrase: key,
|
||||||
|
);
|
||||||
|
Logs().d('Successful elfsigned');
|
||||||
|
await bootstrap.openExistingSsss();
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().w('Unable to unlock SSSS', e, s);
|
||||||
|
setState(
|
||||||
|
() => _recoveryKeyInputError =
|
||||||
|
L10n.of(context)!.oopsSomethingWentWrong,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(
|
||||||
|
() => _recoveryKeyInputLoading = false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Text(L10n.of(context)!.or),
|
||||||
|
),
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.cast_connected_outlined),
|
icon: const Icon(Icons.cast_connected_outlined),
|
||||||
@ -408,11 +414,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
case BootstrapState.error:
|
case BootstrapState.error:
|
||||||
titleText = L10n.of(context)!.oopsSomethingWentWrong;
|
titleText = L10n.of(context)!.oopsSomethingWentWrong;
|
||||||
body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
|
body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
|
||||||
buttons.add(AdaptiveFlatButton(
|
buttons.add(
|
||||||
label: L10n.of(context)!.close,
|
AdaptiveFlatButton(
|
||||||
onPressed: () =>
|
label: L10n.of(context)!.close,
|
||||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
onPressed: () =>
|
||||||
));
|
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case BootstrapState.done:
|
case BootstrapState.done:
|
||||||
titleText = L10n.of(context)!.everythingReady;
|
titleText = L10n.of(context)!.everythingReady;
|
||||||
@ -423,11 +431,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
|
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
buttons.add(AdaptiveFlatButton(
|
buttons.add(
|
||||||
label: L10n.of(context)!.close,
|
AdaptiveFlatButton(
|
||||||
onPressed: () =>
|
label: L10n.of(context)!.close,
|
||||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
onPressed: () =>
|
||||||
));
|
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,8 @@ class AddWidgetTileState extends State<AddWidgetTile> {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)));
|
SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +26,15 @@ class AddWidgetTileView extends StatelessWidget {
|
|||||||
'm.jitsi': Text(L10n.of(context)!.widgetJitsi),
|
'm.jitsi': Text(L10n.of(context)!.widgetJitsi),
|
||||||
'm.video': Text(L10n.of(context)!.widgetVideo),
|
'm.video': Text(L10n.of(context)!.widgetVideo),
|
||||||
'm.custom': Text(L10n.of(context)!.widgetCustom),
|
'm.custom': Text(L10n.of(context)!.widgetCustom),
|
||||||
}.map((key, value) => MapEntry(
|
}.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
key,
|
key,
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: value,
|
child: value,
|
||||||
))),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
onValueChanged: controller.setWidgetType,
|
onValueChanged: controller.setWidgetType,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -82,10 +82,12 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
final matrixFiles = <MatrixFile>[];
|
final matrixFiles = <MatrixFile>[];
|
||||||
for (var i = 0; i < bytesList.result!.length; i++) {
|
for (var i = 0; i < bytesList.result!.length; i++) {
|
||||||
matrixFiles.add(MatrixFile(
|
matrixFiles.add(
|
||||||
bytes: bytesList.result![i],
|
MatrixFile(
|
||||||
name: details.files[i].name,
|
bytes: bytesList.result![i],
|
||||||
).detectFileType);
|
name: details.files[i].name,
|
||||||
|
).detectFileType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@ -139,18 +141,20 @@ class ChatController extends State<Chat> {
|
|||||||
final userId = room?.directChatMatrixID;
|
final userId = room?.directChatMatrixID;
|
||||||
if (room == null || userId == null) {
|
if (room == null || userId == null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Try to recreate a room with is not a DM room. This should not be possible from the UI!');
|
'Try to recreate a room with is not a DM room. This should not be possible from the UI!',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final success = await showFutureLoadingDialog(
|
final success = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
final client = room.client;
|
final client = room.client;
|
||||||
final waitForSync = client.onSync.stream
|
final waitForSync = client.onSync.stream
|
||||||
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
|
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
|
||||||
await room.leave();
|
await room.leave();
|
||||||
await waitForSync;
|
await waitForSync;
|
||||||
return await client.startDirectChat(userId);
|
return await client.startDirectChat(userId);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
final roomId = success.result;
|
final roomId = success.result;
|
||||||
if (roomId == null) return;
|
if (roomId == null) return;
|
||||||
VRouter.of(context).toSegments(['rooms', roomId]);
|
VRouter.of(context).toSegments(['rooms', roomId]);
|
||||||
@ -160,7 +164,8 @@ class ChatController extends State<Chat> {
|
|||||||
final room = this.room;
|
final room = this.room;
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Leave room button clicked while room is null. This should not be possible from the UI!');
|
'Leave room button clicked while room is null. This should not be possible from the UI!',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final success = await showFutureLoadingDialog(
|
final success = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -327,10 +332,12 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
// ignore: unawaited_futures
|
||||||
room!.sendTextEvent(sendController.text,
|
room!.sendTextEvent(
|
||||||
inReplyTo: replyEvent,
|
sendController.text,
|
||||||
editEventId: editEvent?.eventId,
|
inReplyTo: replyEvent,
|
||||||
parseCommands: parseCommands);
|
editEventId: editEvent?.eventId,
|
||||||
|
parseCommands: parseCommands,
|
||||||
|
);
|
||||||
sendController.value = TextEditingValue(
|
sendController.value = TextEditingValue(
|
||||||
text: pendingText,
|
text: pendingText,
|
||||||
selection: const TextSelection.collapsed(offset: 0),
|
selection: const TextSelection.collapsed(offset: 0),
|
||||||
@ -354,10 +361,12 @@ class ChatController extends State<Chat> {
|
|||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (c) => SendFileDialog(
|
builder: (c) => SendFileDialog(
|
||||||
files: result
|
files: result
|
||||||
.map((xfile) => MatrixFile(
|
.map(
|
||||||
bytes: xfile.toUint8List(),
|
(xfile) => MatrixFile(
|
||||||
name: xfile.fileName!,
|
bytes: xfile.toUint8List(),
|
||||||
).detectFileType)
|
name: xfile.fileName!,
|
||||||
|
).detectFileType,
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
room: room!,
|
room: room!,
|
||||||
),
|
),
|
||||||
@ -375,10 +384,12 @@ class ChatController extends State<Chat> {
|
|||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (c) => SendFileDialog(
|
builder: (c) => SendFileDialog(
|
||||||
files: result
|
files: result
|
||||||
.map((xfile) => MatrixFile(
|
.map(
|
||||||
bytes: xfile.toUint8List(),
|
(xfile) => MatrixFile(
|
||||||
name: xfile.fileName!,
|
bytes: xfile.toUint8List(),
|
||||||
).detectFileType)
|
name: xfile.fileName!,
|
||||||
|
).detectFileType,
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
room: room!,
|
room: room!,
|
||||||
),
|
),
|
||||||
@ -537,8 +548,9 @@ class ChatController extends State<Chat> {
|
|||||||
for (final event in selectedEvents) {
|
for (final event in selectedEvents) {
|
||||||
if (copyString.isNotEmpty) copyString += '\n\n';
|
if (copyString.isNotEmpty) copyString += '\n\n';
|
||||||
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: true);
|
withSenderNamePrefix: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return copyString;
|
return copyString;
|
||||||
}
|
}
|
||||||
@ -554,33 +566,35 @@ class ChatController extends State<Chat> {
|
|||||||
void reportEventAction() async {
|
void reportEventAction() async {
|
||||||
final event = selectedEvents.single;
|
final event = selectedEvents.single;
|
||||||
final score = await showConfirmationDialog<int>(
|
final score = await showConfirmationDialog<int>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.reportMessage,
|
title: L10n.of(context)!.reportMessage,
|
||||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
actions: [
|
actions: [
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -100,
|
key: -100,
|
||||||
label: L10n.of(context)!.extremeOffensive,
|
label: L10n.of(context)!.extremeOffensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -50,
|
key: -50,
|
||||||
label: L10n.of(context)!.offensive,
|
label: L10n.of(context)!.offensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: 0,
|
key: 0,
|
||||||
label: L10n.of(context)!.inoffensive,
|
label: L10n.of(context)!.inoffensive,
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
if (score == null) return;
|
if (score == null) return;
|
||||||
final reason = await showTextInputDialog(
|
final reason = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||||
|
);
|
||||||
if (reason == null || reason.single.isEmpty) return;
|
if (reason == null || reason.single.isEmpty) return;
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -597,7 +611,8 @@ class ChatController extends State<Chat> {
|
|||||||
selectedEvents.clear();
|
selectedEvents.clear();
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)));
|
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void redactEventsAction() async {
|
void redactEventsAction() async {
|
||||||
@ -612,25 +627,27 @@ class ChatController extends State<Chat> {
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
for (final event in selectedEvents) {
|
for (final event in selectedEvents) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
if (event.status.isSent) {
|
if (event.status.isSent) {
|
||||||
if (event.canRedact) {
|
if (event.canRedact) {
|
||||||
await event.redactEvent();
|
await event.redactEvent();
|
||||||
} else {
|
|
||||||
final client = currentRoomBundle.firstWhere(
|
|
||||||
(cl) => selectedEvents.first.senderId == cl!.userID,
|
|
||||||
orElse: () => null);
|
|
||||||
if (client == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final room = client.getRoomById(roomId!)!;
|
|
||||||
await Event.fromJson(event.toJson(), room).redactEvent();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await event.remove();
|
final client = currentRoomBundle.firstWhere(
|
||||||
|
(cl) => selectedEvents.first.senderId == cl!.userID,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final room = client.getRoomById(roomId!)!;
|
||||||
|
await Event.fromJson(event.toJson(), room).redactEvent();
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
await event.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
showEmojiPicker = false;
|
showEmojiPicker = false;
|
||||||
@ -706,41 +723,42 @@ class ChatController extends State<Chat> {
|
|||||||
// event id not found...maybe we can fetch it?
|
// event id not found...maybe we can fetch it?
|
||||||
// the try...finally is here to start and close the loading dialog reliably
|
// the try...finally is here to start and close the loading dialog reliably
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
// okay, we first have to fetch if the event is in the room
|
// okay, we first have to fetch if the event is in the room
|
||||||
|
try {
|
||||||
|
final event = await timeline!.getEventById(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
// event is null...meaning something is off
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||||
|
// event wasn't found, as the server gave a 404 or something
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// okay, we know that the event *is* in the room
|
||||||
|
while (eventIndex == -1) {
|
||||||
|
if (!canLoadMore) {
|
||||||
|
// we can't load any more events but still haven't found ours yet...better stop here
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final event = await timeline!.getEventById(eventId);
|
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
||||||
if (event == null) {
|
|
||||||
// event is null...meaning something is off
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
if (err is TimeoutException) {
|
||||||
// event wasn't found, as the server gave a 404 or something
|
// loading the history timed out...so let's do nothing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
// okay, we know that the event *is* in the room
|
eventIndex =
|
||||||
while (eventIndex == -1) {
|
timeline!.events.indexWhere((e) => e.eventId == eventId);
|
||||||
if (!canLoadMore) {
|
}
|
||||||
// we can't load any more events but still haven't found ours yet...better stop here
|
},
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
try {
|
|
||||||
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
|
||||||
} catch (err) {
|
|
||||||
if (err is TimeoutException) {
|
|
||||||
// loading the history timed out...so let's do nothing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
eventIndex =
|
|
||||||
timeline!.events.indexWhere((e) => e.eventId == eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@ -811,7 +829,8 @@ class ChatController extends State<Chat> {
|
|||||||
sendController
|
sendController
|
||||||
..text = sendController.text.characters.skipLast(1).toString()
|
..text = sendController.text.characters.skipLast(1).toString()
|
||||||
..selection = TextSelection.fromPosition(
|
..selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: sendController.text.length));
|
TextPosition(offset: sendController.text.length),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -846,8 +865,9 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
void editSelectedEventAction() {
|
void editSelectedEventAction() {
|
||||||
final client = currentRoomBundle.firstWhere(
|
final client = currentRoomBundle.firstWhere(
|
||||||
(cl) => selectedEvents.first.senderId == cl!.userID,
|
(cl) => selectedEvents.first.senderId == cl!.userID,
|
||||||
orElse: () => null);
|
orElse: () => null,
|
||||||
|
);
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -855,10 +875,12 @@ class ChatController extends State<Chat> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
pendingText = sendController.text;
|
pendingText = sendController.text;
|
||||||
editEvent = selectedEvents.first;
|
editEvent = selectedEvents.first;
|
||||||
inputText = sendController.text = editEvent!
|
inputText = sendController.text =
|
||||||
.getDisplayEvent(timeline!)
|
editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
||||||
.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: false, hideReply: true);
|
withSenderNamePrefix: false,
|
||||||
|
hideReply: true,
|
||||||
|
);
|
||||||
selectedEvents.clear();
|
selectedEvents.clear();
|
||||||
});
|
});
|
||||||
inputFocus.requestFocus();
|
inputFocus.requestFocus();
|
||||||
@ -881,10 +903,12 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => room!.client.joinRoom(room!
|
future: () => room!.client.joinRoom(
|
||||||
.getState(EventTypes.RoomTombstone)!
|
room!
|
||||||
.parsedTombstoneContent
|
.getState(EventTypes.RoomTombstone)!
|
||||||
.replacementRoom),
|
.parsedTombstoneContent
|
||||||
|
.replacementRoom,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -1077,9 +1101,10 @@ class ChatController extends State<Chat> {
|
|||||||
if (callType == null) return;
|
if (callType == null) return;
|
||||||
|
|
||||||
final success = await showFutureLoadingDialog(
|
final success = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () =>
|
future: () =>
|
||||||
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials());
|
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials(),
|
||||||
|
);
|
||||||
if (success.result != null) {
|
if (success.result != null) {
|
||||||
final voipPlugin = Matrix.of(context).voipPlugin;
|
final voipPlugin = Matrix.of(context).voipPlugin;
|
||||||
try {
|
try {
|
||||||
|
@ -86,19 +86,20 @@ class ChatEventList extends StatelessWidget {
|
|||||||
index: i - 1,
|
index: i - 1,
|
||||||
controller: controller.scrollController,
|
controller: controller.scrollController,
|
||||||
child: event.isVisibleInGui
|
child: event.isVisibleInGui
|
||||||
? Message(event,
|
? Message(
|
||||||
|
event,
|
||||||
onSwipe: (direction) =>
|
onSwipe: (direction) =>
|
||||||
controller.replyAction(replyTo: event),
|
controller.replyAction(replyTo: event),
|
||||||
onInfoTab: controller.showEventInfo,
|
onInfoTab: controller.showEventInfo,
|
||||||
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
|
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (c) => UserBottomSheet(
|
builder: (c) => UserBottomSheet(
|
||||||
user: event.senderFromMemoryOrFallback,
|
user: event.senderFromMemoryOrFallback,
|
||||||
outerContext: context,
|
outerContext: context,
|
||||||
onMention: () => controller.sendController.text +=
|
onMention: () => controller.sendController.text +=
|
||||||
'${event.senderFromMemoryOrFallback.mention} ',
|
'${event.senderFromMemoryOrFallback.mention} ',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onSelect: controller.onSelectMessage,
|
onSelect: controller.onSelectMessage,
|
||||||
scrollToEventId: (String eventId) =>
|
scrollToEventId: (String eventId) =>
|
||||||
controller.scrollToEventId(eventId),
|
controller.scrollToEventId(eventId),
|
||||||
@ -108,7 +109,8 @@ class ChatEventList extends StatelessWidget {
|
|||||||
timeline: controller.timeline!,
|
timeline: controller.timeline!,
|
||||||
nextEvent: i < controller.timeline!.events.length
|
nextEvent: i < controller.timeline!.events.length
|
||||||
? controller.timeline!.events[i]
|
? controller.timeline!.events[i]
|
||||||
: null)
|
: null,
|
||||||
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -300,23 +300,24 @@ class _ChatAccountPicker extends StatelessWidget {
|
|||||||
builder: (context, snapshot) => PopupMenuButton<String>(
|
builder: (context, snapshot) => PopupMenuButton<String>(
|
||||||
onSelected: _popupMenuButtonSelected,
|
onSelected: _popupMenuButtonSelected,
|
||||||
itemBuilder: (BuildContext context) => clients
|
itemBuilder: (BuildContext context) => clients
|
||||||
.map((client) => PopupMenuItem<String>(
|
.map(
|
||||||
value: client!.userID,
|
(client) => PopupMenuItem<String>(
|
||||||
child: FutureBuilder<Profile>(
|
value: client!.userID,
|
||||||
future: client.fetchOwnProfile(),
|
child: FutureBuilder<Profile>(
|
||||||
builder: (context, snapshot) => ListTile(
|
future: client.fetchOwnProfile(),
|
||||||
leading: Avatar(
|
builder: (context, snapshot) => ListTile(
|
||||||
mxContent: snapshot.data?.avatarUrl,
|
leading: Avatar(
|
||||||
name: snapshot.data?.displayName ??
|
mxContent: snapshot.data?.avatarUrl,
|
||||||
client.userID!.localpart,
|
name: snapshot.data?.displayName ??
|
||||||
size: 20,
|
client.userID!.localpart,
|
||||||
),
|
size: 20,
|
||||||
title:
|
|
||||||
Text(snapshot.data?.displayName ?? client.userID!),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
),
|
||||||
|
title: Text(snapshot.data?.displayName ?? client.userID!),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
),
|
),
|
||||||
))
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
child: Avatar(
|
child: Avatar(
|
||||||
mxContent: snapshot.data?.avatarUrl,
|
mxContent: snapshot.data?.avatarUrl,
|
||||||
|
@ -49,11 +49,12 @@ class ChatView extends StatelessWidget {
|
|||||||
if (controller.canSaveSelectedEvent)
|
if (controller.canSaveSelectedEvent)
|
||||||
// Use builder context to correctly position the share dialog on iPad
|
// Use builder context to correctly position the share dialog on iPad
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) => IconButton(
|
builder: (context) => IconButton(
|
||||||
icon: Icon(Icons.adaptive.share),
|
icon: Icon(Icons.adaptive.share),
|
||||||
tooltip: L10n.of(context)!.share,
|
tooltip: L10n.of(context)!.share,
|
||||||
onPressed: () => controller.saveSelectedEvent(context),
|
onPressed: () => controller.saveSelectedEvent(context),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
if (controller.canRedactSelectedEvents)
|
if (controller.canRedactSelectedEvents)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outlined),
|
icon: const Icon(Icons.delete_outlined),
|
||||||
@ -155,7 +156,9 @@ class ChatView extends StatelessWidget {
|
|||||||
|
|
||||||
if (controller.room!.membership == Membership.invite) {
|
if (controller.room!.membership == Membership.invite) {
|
||||||
showFutureLoadingDialog(
|
showFutureLoadingDialog(
|
||||||
context: context, future: () => controller.room!.join());
|
context: context,
|
||||||
|
future: () => controller.room!.join(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
@ -249,21 +252,23 @@ class ChatView extends StatelessWidget {
|
|||||||
PinnedEvents(controller),
|
PinnedEvents(controller),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: controller.clearSingleSelectedEvent,
|
onTap: controller.clearSingleSelectedEvent,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
if (controller.timeline == null) {
|
if (controller.timeline == null) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator
|
child:
|
||||||
.adaptive(strokeWidth: 2),
|
CircularProgressIndicator.adaptive(
|
||||||
);
|
strokeWidth: 2,),
|
||||||
}
|
|
||||||
|
|
||||||
return ChatEventList(
|
|
||||||
controller: controller,
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
)),
|
|
||||||
|
return ChatEventList(
|
||||||
|
controller: controller,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (controller.room!.canSendDefaultMessages &&
|
if (controller.room!.canSendDefaultMessages &&
|
||||||
controller.room!.membership == Membership.join)
|
controller.room!.membership == Membership.join)
|
||||||
@ -274,7 +279,8 @@ class ChatView extends StatelessWidget {
|
|||||||
right: bottomSheetPadding,
|
right: bottomSheetPadding,
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: FluffyThemes.columnWidth * 2.5),
|
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||||
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
@ -324,7 +330,8 @@ class ChatView extends StatelessWidget {
|
|||||||
onPressed:
|
onPressed:
|
||||||
controller.recreateChat,
|
controller.recreateChat,
|
||||||
label: Text(
|
label: Text(
|
||||||
L10n.of(context)!.reopenChat),
|
L10n.of(context)!.reopenChat,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -15,15 +15,18 @@ class EditWidgetsDialog extends StatelessWidget {
|
|||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
title: Text(L10n.of(context)!.editWidgets),
|
title: Text(L10n.of(context)!.editWidgets),
|
||||||
children: [
|
children: [
|
||||||
...room.widgets.map((e) => ListTile(
|
...room.widgets.map(
|
||||||
title: Text(e.name ?? e.type),
|
(e) => ListTile(
|
||||||
leading: IconButton(
|
title: Text(e.name ?? e.type),
|
||||||
onPressed: () {
|
leading: IconButton(
|
||||||
room.deleteWidget(e.id!);
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
room.deleteWidget(e.id!);
|
||||||
},
|
Navigator.of(context).pop();
|
||||||
icon: const Icon(Icons.delete)),
|
},
|
||||||
)),
|
icon: const Icon(Icons.delete),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
AddWidgetTile(room: room),
|
AddWidgetTile(room: room),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -13,34 +13,34 @@ class EncryptionButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return StreamBuilder<SyncUpdate>(
|
return StreamBuilder<SyncUpdate>(
|
||||||
stream: Matrix.of(context)
|
stream: Matrix.of(context)
|
||||||
.client
|
.client
|
||||||
.onSync
|
.onSync
|
||||||
.stream
|
.stream
|
||||||
.where((s) => s.deviceLists != null),
|
.where((s) => s.deviceLists != null),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return FutureBuilder<EncryptionHealthState>(
|
return FutureBuilder<EncryptionHealthState>(
|
||||||
future: room.calcEncryptionHealthState(),
|
future: room.calcEncryptionHealthState(),
|
||||||
builder: (BuildContext context, snapshot) => IconButton(
|
builder: (BuildContext context, snapshot) => IconButton(
|
||||||
tooltip: room.encrypted
|
tooltip: room.encrypted
|
||||||
? L10n.of(context)!.encrypted
|
? L10n.of(context)!.encrypted
|
||||||
: L10n.of(context)!.encryptionNotEnabled,
|
: L10n.of(context)!.encryptionNotEnabled,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
room.encrypted
|
room.encrypted ? Icons.lock_outlined : Icons.lock_open_outlined,
|
||||||
? Icons.lock_outlined
|
size: 20,
|
||||||
: Icons.lock_open_outlined,
|
color: room.joinRules != JoinRules.public && !room.encrypted
|
||||||
size: 20,
|
? Colors.red
|
||||||
color: room.joinRules != JoinRules.public &&
|
: room.joinRules != JoinRules.public &&
|
||||||
!room.encrypted
|
snapshot.data ==
|
||||||
? Colors.red
|
EncryptionHealthState.unverifiedDevices
|
||||||
: room.joinRules != JoinRules.public &&
|
? Colors.orange
|
||||||
snapshot.data ==
|
: null,
|
||||||
EncryptionHealthState.unverifiedDevices
|
),
|
||||||
? Colors.orange
|
onPressed: () => VRouter.of(context)
|
||||||
: null),
|
.toSegments(['rooms', room.id, 'encryption']),
|
||||||
onPressed: () => VRouter.of(context)
|
),
|
||||||
.toSegments(['rooms', room.id, 'encryption']),
|
);
|
||||||
));
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,8 @@ class EventInfoDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.sender),
|
title: Text(L10n.of(context)!.sender),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]'),
|
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10n.of(context)!.time),
|
title: Text(L10n.of(context)!.time),
|
||||||
|
@ -69,7 +69,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final fileName = Uri.encodeComponent(
|
final fileName = Uri.encodeComponent(
|
||||||
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last);
|
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||||
|
);
|
||||||
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
|
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
|
||||||
await file.writeAsBytes(matrixFile.bytes);
|
await file.writeAsBytes(matrixFile.bytes);
|
||||||
}
|
}
|
||||||
@ -224,23 +225,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||||||
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => audioPlayer?.seek(Duration(
|
onTap: () => audioPlayer?.seek(
|
||||||
|
Duration(
|
||||||
milliseconds:
|
milliseconds:
|
||||||
(maxPosition / AudioPlayerWidget.wavesCount)
|
(maxPosition / AudioPlayerWidget.wavesCount)
|
||||||
.round() *
|
.round() *
|
||||||
i)),
|
i,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 32,
|
height: 32,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: currentPosition > i ? 1 : 0.5,
|
opacity: currentPosition > i ? 1 : 0.5,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: widget.color,
|
color: widget.color,
|
||||||
borderRadius: BorderRadius.circular(64),
|
borderRadius: BorderRadius.circular(64),
|
||||||
),
|
),
|
||||||
height: 32 * (waveform[i] / 1024)),
|
height: 32 * (waveform[i] / 1024),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -38,9 +38,14 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
||||||
// strip it.
|
// strip it.
|
||||||
final renderHtml = html.replaceAll(
|
final renderHtml = html.replaceAll(
|
||||||
RegExp('<mx-reply>.*</mx-reply>',
|
RegExp(
|
||||||
caseSensitive: false, multiLine: false, dotAll: true),
|
'<mx-reply>.*</mx-reply>',
|
||||||
'');
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
// there is no need to pre-validate the html, as we validate it while rendering
|
// there is no need to pre-validate the html, as we validate it while rendering
|
||||||
|
|
||||||
@ -61,8 +66,12 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||||
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
|
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||||
getMxcUrl: (String mxc, double? width, double? height,
|
getMxcUrl: (
|
||||||
{bool? animated = false}) {
|
String mxc,
|
||||||
|
double? width,
|
||||||
|
double? height, {
|
||||||
|
bool? animated = false,
|
||||||
|
}) {
|
||||||
final ratio = MediaQuery.of(context).devicePixelRatio;
|
final ratio = MediaQuery.of(context).devicePixelRatio;
|
||||||
return Uri.parse(mxc)
|
return Uri.parse(mxc)
|
||||||
.getThumbnail(
|
.getThumbnail(
|
||||||
@ -75,19 +84,23 @@ class HtmlMessage extends StatelessWidget {
|
|||||||
.toString();
|
.toString();
|
||||||
},
|
},
|
||||||
onImageTap: (String mxc) => showDialog(
|
onImageTap: (String mxc) => showDialog(
|
||||||
context: Matrix.of(context).navigatorContext,
|
context: Matrix.of(context).navigatorContext,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (_) => ImageViewer(Event(
|
builder: (_) => ImageViewer(
|
||||||
type: EventTypes.Message,
|
Event(
|
||||||
content: <String, dynamic>{
|
type: EventTypes.Message,
|
||||||
'body': mxc,
|
content: <String, dynamic>{
|
||||||
'url': mxc,
|
'body': mxc,
|
||||||
'msgtype': MessageTypes.Image,
|
'url': mxc,
|
||||||
},
|
'msgtype': MessageTypes.Image,
|
||||||
senderId: room.client.userID!,
|
},
|
||||||
originServerTs: DateTime.now(),
|
senderId: room.client.userID!,
|
||||||
eventId: 'fake_event',
|
originServerTs: DateTime.now(),
|
||||||
room: room))),
|
eventId: 'fake_event',
|
||||||
|
room: room,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
setCodeLanguage: (String key, String value) async {
|
setCodeLanguage: (String key, String value) async {
|
||||||
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
|
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
|
||||||
},
|
},
|
||||||
|
@ -27,18 +27,19 @@ class Message extends StatelessWidget {
|
|||||||
final bool selected;
|
final bool selected;
|
||||||
final Timeline timeline;
|
final Timeline timeline;
|
||||||
|
|
||||||
const Message(this.event,
|
const Message(
|
||||||
{this.nextEvent,
|
this.event, {
|
||||||
this.longPressSelect = false,
|
this.nextEvent,
|
||||||
this.onSelect,
|
this.longPressSelect = false,
|
||||||
this.onInfoTab,
|
this.onSelect,
|
||||||
this.onAvatarTab,
|
this.onInfoTab,
|
||||||
this.scrollToEventId,
|
this.onAvatarTab,
|
||||||
required this.onSwipe,
|
this.scrollToEventId,
|
||||||
this.selected = false,
|
required this.onSwipe,
|
||||||
required this.timeline,
|
this.selected = false,
|
||||||
Key? key})
|
required this.timeline,
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Indicates wheither the user may use a mouse instead
|
/// Indicates wheither the user may use a mouse instead
|
||||||
/// of touchscreen.
|
/// of touchscreen.
|
||||||
@ -126,13 +127,15 @@ class Message extends StatelessWidget {
|
|||||||
height: 16 * AppConfig.bubbleSizeFactor,
|
height: 16 * AppConfig.bubbleSizeFactor,
|
||||||
child: event.status == EventStatus.sending
|
child: event.status == EventStatus.sending
|
||||||
? const CircularProgressIndicator.adaptive(
|
? const CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2)
|
strokeWidth: 2,
|
||||||
|
)
|
||||||
: event.status == EventStatus.error
|
: event.status == EventStatus.error
|
||||||
? const Icon(Icons.error, color: Colors.red)
|
? const Icon(Icons.error, color: Colors.red)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
: FutureBuilder<User?>(
|
: FutureBuilder<User?>(
|
||||||
future: event.fetchSenderUser(),
|
future: event.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@ -142,7 +145,8 @@ class Message extends StatelessWidget {
|
|||||||
name: user.calcDisplayname(),
|
name: user.calcDisplayname(),
|
||||||
onTap: () => onAvatarTab!(event),
|
onTap: () => onAvatarTab!(event),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -171,7 +175,8 @@ class Message extends StatelessWidget {
|
|||||||
: displayname.lightColorText),
|
: displayname.lightColorText),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
@ -198,7 +203,8 @@ class Message extends StatelessWidget {
|
|||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
|
: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: FluffyThemes.columnWidth * 1.5),
|
maxWidth: FluffyThemes.columnWidth * 1.5,
|
||||||
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
@ -233,11 +239,14 @@ class Message extends StatelessWidget {
|
|||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
vertical: 4.0 *
|
vertical:
|
||||||
AppConfig.bubbleSizeFactor),
|
4.0 * AppConfig.bubbleSizeFactor,
|
||||||
child: ReplyContent(replyEvent,
|
),
|
||||||
ownMessage: ownMessage,
|
child: ReplyContent(
|
||||||
timeline: timeline),
|
replyEvent,
|
||||||
|
ownMessage: ownMessage,
|
||||||
|
timeline: timeline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -249,10 +258,13 @@ class Message extends StatelessWidget {
|
|||||||
onInfoTab: onInfoTab,
|
onInfoTab: onInfoTab,
|
||||||
),
|
),
|
||||||
if (event.hasAggregatedEvents(
|
if (event.hasAggregatedEvents(
|
||||||
timeline, RelationshipTypes.edit))
|
timeline,
|
||||||
|
RelationshipTypes.edit,
|
||||||
|
))
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 4.0 * AppConfig.bubbleSizeFactor),
|
top: 4.0 * AppConfig.bubbleSizeFactor,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -301,26 +313,29 @@ class Message extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: displayTime
|
padding: displayTime
|
||||||
? EdgeInsets.symmetric(
|
? EdgeInsets.symmetric(
|
||||||
vertical: 8.0 * AppConfig.bubbleSizeFactor)
|
vertical: 8.0 * AppConfig.bubbleSizeFactor,
|
||||||
|
)
|
||||||
: EdgeInsets.zero,
|
: EdgeInsets.zero,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Material(
|
child: Material(
|
||||||
color: displayTime
|
color: displayTime
|
||||||
? Theme.of(context).colorScheme.background
|
? Theme.of(context).colorScheme.background
|
||||||
: Theme.of(context)
|
: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.background
|
.background
|
||||||
.withOpacity(0.33),
|
.withOpacity(0.33),
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
borderRadius:
|
||||||
clipBehavior: Clip.antiAlias,
|
BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
child: Padding(
|
clipBehavior: Clip.antiAlias,
|
||||||
padding: const EdgeInsets.all(6.0),
|
child: Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.all(6.0),
|
||||||
event.originServerTs.localizedTime(context),
|
child: Text(
|
||||||
style: TextStyle(fontSize: 14 * AppConfig.fontSizeFactor),
|
event.originServerTs.localizedTime(context),
|
||||||
|
style: TextStyle(fontSize: 14 * AppConfig.fontSizeFactor),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
),
|
),
|
||||||
row,
|
row,
|
||||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction))
|
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction))
|
||||||
|
@ -27,21 +27,27 @@ class MessageContent extends StatelessWidget {
|
|||||||
final Color textColor;
|
final Color textColor;
|
||||||
final void Function(Event)? onInfoTab;
|
final void Function(Event)? onInfoTab;
|
||||||
|
|
||||||
const MessageContent(this.event,
|
const MessageContent(
|
||||||
{this.onInfoTab, Key? key, required this.textColor})
|
this.event, {
|
||||||
: super(key: key);
|
this.onInfoTab,
|
||||||
|
Key? key,
|
||||||
|
required this.textColor,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
void _verifyOrRequestKey(BuildContext context) async {
|
void _verifyOrRequestKey(BuildContext context) async {
|
||||||
final l10n = L10n.of(context)!;
|
final l10n = L10n.of(context)!;
|
||||||
if (event.content['can_request_session'] != true) {
|
if (event.content['can_request_session'] != true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
event.type == EventTypes.Encrypted
|
event.type == EventTypes.Encrypted
|
||||||
? l10n.needPantalaimonWarning
|
? l10n.needPantalaimonWarning
|
||||||
: event.calcLocalizedBodyFallback(
|
: event.calcLocalizedBodyFallback(
|
||||||
MatrixLocals(l10n),
|
MatrixLocals(l10n),
|
||||||
),
|
),
|
||||||
)));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
@ -213,73 +219,83 @@ class MessageContent extends StatelessWidget {
|
|||||||
default:
|
default:
|
||||||
if (event.redacted) {
|
if (event.redacted) {
|
||||||
return FutureBuilder<User?>(
|
return FutureBuilder<User?>(
|
||||||
future: event.redactedBecause?.fetchSenderUser(),
|
future: event.redactedBecause?.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return _ButtonContent(
|
return _ButtonContent(
|
||||||
label: L10n.of(context)!.redactedAnEvent(snapshot.data
|
label: L10n.of(context)!.redactedAnEvent(
|
||||||
?.calcDisplayname() ??
|
snapshot.data?.calcDisplayname() ??
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname()),
|
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||||
icon: const Icon(Icons.delete_outlined),
|
),
|
||||||
textColor: buttonTextColor,
|
icon: const Icon(Icons.delete_outlined),
|
||||||
onPressed: () => onInfoTab!(event),
|
textColor: buttonTextColor,
|
||||||
);
|
onPressed: () => onInfoTab!(event),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final bigEmotes = event.onlyEmotes &&
|
final bigEmotes = event.onlyEmotes &&
|
||||||
event.numberEmotes > 0 &&
|
event.numberEmotes > 0 &&
|
||||||
event.numberEmotes <= 10;
|
event.numberEmotes <= 10;
|
||||||
return FutureBuilder<String>(
|
return FutureBuilder<String>(
|
||||||
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!),
|
future: event.calcLocalizedBody(
|
||||||
hideReply: true),
|
MatrixLocals(L10n.of(context)!),
|
||||||
builder: (context, snapshot) {
|
hideReply: true,
|
||||||
return LinkText(
|
),
|
||||||
text: snapshot.data ??
|
builder: (context, snapshot) {
|
||||||
event.calcLocalizedBodyFallback(
|
return LinkText(
|
||||||
MatrixLocals(L10n.of(context)!),
|
text: snapshot.data ??
|
||||||
hideReply: true),
|
event.calcLocalizedBodyFallback(
|
||||||
textStyle: TextStyle(
|
MatrixLocals(L10n.of(context)!),
|
||||||
color: textColor,
|
hideReply: true,
|
||||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
),
|
||||||
decoration:
|
textStyle: TextStyle(
|
||||||
event.redacted ? TextDecoration.lineThrough : null,
|
color: textColor,
|
||||||
),
|
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||||
linkStyle: TextStyle(
|
decoration:
|
||||||
color: textColor.withAlpha(150),
|
event.redacted ? TextDecoration.lineThrough : null,
|
||||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
),
|
||||||
decoration: TextDecoration.underline,
|
linkStyle: TextStyle(
|
||||||
decorationColor: textColor.withAlpha(150),
|
color: textColor.withAlpha(150),
|
||||||
),
|
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||||
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
decoration: TextDecoration.underline,
|
||||||
);
|
decorationColor: textColor.withAlpha(150),
|
||||||
});
|
),
|
||||||
|
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case EventTypes.CallInvite:
|
case EventTypes.CallInvite:
|
||||||
return FutureBuilder<User?>(
|
return FutureBuilder<User?>(
|
||||||
future: event.fetchSenderUser(),
|
future: event.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return _ButtonContent(
|
return _ButtonContent(
|
||||||
label: L10n.of(context)!.startedACall(
|
label: L10n.of(context)!.startedACall(
|
||||||
snapshot.data?.calcDisplayname() ??
|
snapshot.data?.calcDisplayname() ??
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname()),
|
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||||
icon: const Icon(Icons.phone_outlined),
|
),
|
||||||
textColor: buttonTextColor,
|
icon: const Icon(Icons.phone_outlined),
|
||||||
onPressed: () => onInfoTab!(event),
|
textColor: buttonTextColor,
|
||||||
);
|
onPressed: () => onInfoTab!(event),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return FutureBuilder<User?>(
|
return FutureBuilder<User?>(
|
||||||
future: event.fetchSenderUser(),
|
future: event.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return _ButtonContent(
|
return _ButtonContent(
|
||||||
label: L10n.of(context)!.userSentUnknownEvent(
|
label: L10n.of(context)!.userSentUnknownEvent(
|
||||||
snapshot.data?.calcDisplayname() ??
|
snapshot.data?.calcDisplayname() ??
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(),
|
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||||
event.type),
|
event.type,
|
||||||
icon: const Icon(Icons.info_outlined),
|
),
|
||||||
textColor: buttonTextColor,
|
icon: const Icon(Icons.info_outlined),
|
||||||
onPressed: () => onInfoTab!(event),
|
textColor: buttonTextColor,
|
||||||
);
|
onPressed: () => onInfoTab!(event),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,45 +46,51 @@ class MessageReactions extends StatelessWidget {
|
|||||||
|
|
||||||
final reactionList = reactionMap.values.toList();
|
final reactionList = reactionMap.values.toList();
|
||||||
reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
|
reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
|
||||||
return Wrap(spacing: 4.0, runSpacing: 4.0, children: [
|
return Wrap(
|
||||||
...reactionList
|
spacing: 4.0,
|
||||||
.map(
|
runSpacing: 4.0,
|
||||||
(r) => _Reaction(
|
children: [
|
||||||
reactionKey: r.key,
|
...reactionList
|
||||||
count: r.count,
|
.map(
|
||||||
reacted: r.reacted,
|
(r) => _Reaction(
|
||||||
onTap: () {
|
reactionKey: r.key,
|
||||||
if (r.reacted) {
|
count: r.count,
|
||||||
final evt = allReactionEvents.firstWhereOrNull((e) =>
|
reacted: r.reacted,
|
||||||
e.senderId == e.room.client.userID &&
|
onTap: () {
|
||||||
e.content['m.relates_to']['key'] == r.key);
|
if (r.reacted) {
|
||||||
if (evt != null) {
|
final evt = allReactionEvents.firstWhereOrNull(
|
||||||
showFutureLoadingDialog(
|
(e) =>
|
||||||
context: context,
|
e.senderId == e.room.client.userID &&
|
||||||
future: () => evt.redactEvent(),
|
e.content['m.relates_to']['key'] == r.key,
|
||||||
);
|
);
|
||||||
|
if (evt != null) {
|
||||||
|
showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () => evt.redactEvent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.room.sendReaction(event.eventId, r.key!);
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
event.room.sendReaction(event.eventId, r.key!);
|
onLongPress: () async => await _AdaptableReactorsDialog(
|
||||||
}
|
client: client,
|
||||||
},
|
reactionEntry: r,
|
||||||
onLongPress: () async => await _AdaptableReactorsDialog(
|
).show(context),
|
||||||
client: client,
|
),
|
||||||
reactionEntry: r,
|
)
|
||||||
).show(context),
|
.toList(),
|
||||||
|
if (allReactionEvents.any((e) => e.status.isSending))
|
||||||
|
const SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(4.0),
|
||||||
|
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
if (allReactionEvents.any((e) => e.status.isSending))
|
|
||||||
const SizedBox(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(4.0),
|
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,11 +127,13 @@ class _Reaction extends StatelessWidget {
|
|||||||
height: fontSize,
|
height: fontSize,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(count.toString(),
|
Text(
|
||||||
style: TextStyle(
|
count.toString(),
|
||||||
color: textColor,
|
style: TextStyle(
|
||||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
color: textColor,
|
||||||
)),
|
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -133,11 +141,13 @@ class _Reaction extends StatelessWidget {
|
|||||||
if (renderKey.length > 10) {
|
if (renderKey.length > 10) {
|
||||||
renderKey = renderKey.getRange(0, 9) + Characters('…');
|
renderKey = renderKey.getRange(0, 9) + Characters('…');
|
||||||
}
|
}
|
||||||
content = Text('$renderKey $count',
|
content = Text(
|
||||||
style: TextStyle(
|
'$renderKey $count',
|
||||||
color: textColor,
|
style: TextStyle(
|
||||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
color: textColor,
|
||||||
));
|
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onTap != null ? onTap!() : null,
|
onTap: () => onTap != null ? onTap!() : null,
|
||||||
|
@ -84,21 +84,22 @@ class ReplyContent extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FutureBuilder<User?>(
|
FutureBuilder<User?>(
|
||||||
future: displayEvent.fetchSenderUser(),
|
future: displayEvent.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Text(
|
return Text(
|
||||||
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
|
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: ownMessage
|
color: ownMessage
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
? Theme.of(context).colorScheme.onPrimary
|
||||||
: Theme.of(context).colorScheme.onBackground,
|
: Theme.of(context).colorScheme.onBackground,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
replyBody,
|
replyBody,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -25,22 +25,23 @@ class StateMessage extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
),
|
),
|
||||||
child: FutureBuilder<String>(
|
child: FutureBuilder<String>(
|
||||||
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
|
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Text(
|
return Text(
|
||||||
snapshot.data ??
|
snapshot.data ??
|
||||||
event.calcLocalizedBodyFallback(
|
event.calcLocalizedBodyFallback(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
decoration:
|
decoration:
|
||||||
event.redacted ? TextDecoration.lineThrough : null,
|
event.redacted ? TextDecoration.lineThrough : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -9,9 +9,11 @@ class VerificationRequestContent extends StatelessWidget {
|
|||||||
final Event event;
|
final Event event;
|
||||||
final Timeline timeline;
|
final Timeline timeline;
|
||||||
|
|
||||||
const VerificationRequestContent(
|
const VerificationRequestContent({
|
||||||
{required this.event, required this.timeline, Key? key})
|
required this.event,
|
||||||
: super(key: key);
|
required this.timeline,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -43,18 +45,22 @@ class VerificationRequestContent extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.lock_outlined,
|
Icon(
|
||||||
color: canceled
|
Icons.lock_outlined,
|
||||||
? Colors.red
|
color: canceled
|
||||||
: (fullyDone ? Colors.green : Colors.grey)),
|
? Colors.red
|
||||||
|
: (fullyDone ? Colors.green : Colors.grey),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(canceled
|
Text(
|
||||||
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
|
canceled
|
||||||
: (fullyDone
|
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
|
||||||
? L10n.of(context)!.verifySuccess
|
: (fullyDone
|
||||||
: (started
|
? L10n.of(context)!.verifySuccess
|
||||||
? L10n.of(context)!.loadingPleaseWait
|
: (started
|
||||||
: L10n.of(context)!.newVerificationRequest)))
|
? L10n.of(context)!.loadingPleaseWait
|
||||||
|
: L10n.of(context)!.newVerificationRequest)),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -39,7 +39,8 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||||||
} else {
|
} else {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final fileName = Uri.encodeComponent(
|
final fileName = Uri.encodeComponent(
|
||||||
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last);
|
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||||
|
);
|
||||||
final file = File('${tempDir.path}/${fileName}_${videoFile.name}');
|
final file = File('${tempDir.path}/${fileName}_${videoFile.name}');
|
||||||
if (await file.exists() == false) {
|
if (await file.exists() == false) {
|
||||||
await file.writeAsBytes(videoFile.bytes);
|
await file.writeAsBytes(videoFile.bytes);
|
||||||
@ -62,13 +63,17 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on MatrixConnectionException catch (e) {
|
} on MatrixConnectionException catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(e.toLocalizedString(context)),
|
SnackBar(
|
||||||
));
|
content: Text(e.toLocalizedString(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(e.toLocalizedString(context)),
|
SnackBar(
|
||||||
));
|
content: Text(e.toLocalizedString(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
Logs().w('Error while playing video', e, s);
|
Logs().w('Error while playing video', e, s);
|
||||||
} finally {
|
} finally {
|
||||||
// Workaround for Chewie needs time to get the aspectRatio
|
// Workaround for Chewie needs time to get the aspectRatio
|
||||||
@ -120,14 +125,16 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2),
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.download_outlined),
|
: const Icon(Icons.download_outlined),
|
||||||
label: Text(
|
label: Text(
|
||||||
_isDownloading
|
_isDownloading
|
||||||
? L10n.of(context)!.loadingPleaseWait
|
? L10n.of(context)!.loadingPleaseWait
|
||||||
: L10n.of(context)!.videoWithSize(
|
: L10n.of(context)!.videoWithSize(
|
||||||
widget.event.sizeString ?? '?MB'),
|
widget.event.sizeString ?? '?MB',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: _isDownloading ? null : _downloadAction,
|
onPressed: _isDownloading ? null : _downloadAction,
|
||||||
),
|
),
|
||||||
|
@ -117,8 +117,10 @@ class InputBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
// aside of emote packs, also propose normal (tm) unicode emojis
|
// aside of emote packs, also propose normal (tm) unicode emojis
|
||||||
final matchingUnicodeEmojis = Emoji.all()
|
final matchingUnicodeEmojis = Emoji.all()
|
||||||
.where((element) => [element.name, ...element.keywords]
|
.where(
|
||||||
.any((element) => element.toLowerCase().contains(emoteSearch)))
|
(element) => [element.name, ...element.keywords]
|
||||||
|
.any((element) => element.toLowerCase().contains(emoteSearch)),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
// sort by the index of the search term in the name in order to have
|
// sort by the index of the search term in the name in order to have
|
||||||
// best matches first
|
// best matches first
|
||||||
@ -186,12 +188,14 @@ class InputBar extends StatelessWidget {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.contains(roomSearch)) ||
|
.contains(roomSearch)) ||
|
||||||
(state.content['alt_aliases'] is List &&
|
(state.content['alt_aliases'] is List &&
|
||||||
state.content['alt_aliases'].any((l) =>
|
state.content['alt_aliases'].any(
|
||||||
l is String &&
|
(l) =>
|
||||||
l
|
l is String &&
|
||||||
.split(':')[0]
|
l
|
||||||
.toLowerCase()
|
.split(':')[0]
|
||||||
.contains(roomSearch))))) ||
|
.toLowerCase()
|
||||||
|
.contains(roomSearch),
|
||||||
|
)))) ||
|
||||||
(r.name.toLowerCase().contains(roomSearch))) {
|
(r.name.toLowerCase().contains(roomSearch))) {
|
||||||
ret.add({
|
ret.add({
|
||||||
'type': 'room',
|
'type': 'room',
|
||||||
@ -226,8 +230,10 @@ class InputBar extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('/$command',
|
Text(
|
||||||
style: const TextStyle(fontFamily: 'monospace')),
|
'/$command',
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
hint,
|
hint,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -273,8 +279,8 @@ class InputBar extends StatelessWidget {
|
|||||||
child: suggestion['pack_avatar_url'] != null
|
child: suggestion['pack_avatar_url'] != null
|
||||||
? Avatar(
|
? Avatar(
|
||||||
mxContent: Uri.tryParse(
|
mxContent: Uri.tryParse(
|
||||||
suggestion.tryGet<String>('pack_avatar_url') ??
|
suggestion.tryGet<String>('pack_avatar_url') ?? '',
|
||||||
''),
|
),
|
||||||
name: suggestion.tryGet<String>('pack_display_name'),
|
name: suggestion.tryGet<String>('pack_display_name'),
|
||||||
size: size * 0.9,
|
size: size * 0.9,
|
||||||
client: client,
|
client: client,
|
||||||
@ -397,23 +403,27 @@ class InputBar extends StatelessWidget {
|
|||||||
actions: !useShortCuts
|
actions: !useShortCuts
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
NewLineIntent: CallbackAction(onInvoke: (i) {
|
NewLineIntent: CallbackAction(
|
||||||
final val = controller!.value;
|
onInvoke: (i) {
|
||||||
final selection = val.selection.start;
|
final val = controller!.value;
|
||||||
final messageWithoutNewLine =
|
final selection = val.selection.start;
|
||||||
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
|
final messageWithoutNewLine =
|
||||||
controller!.value = TextEditingValue(
|
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
|
||||||
text: messageWithoutNewLine,
|
controller!.value = TextEditingValue(
|
||||||
selection: TextSelection.fromPosition(
|
text: messageWithoutNewLine,
|
||||||
TextPosition(offset: selection + 1),
|
selection: TextSelection.fromPosition(
|
||||||
),
|
TextPosition(offset: selection + 1),
|
||||||
);
|
),
|
||||||
return null;
|
);
|
||||||
}),
|
return null;
|
||||||
SubmitLineIntent: CallbackAction(onInvoke: (i) {
|
},
|
||||||
onSubmitted!(controller!.text);
|
),
|
||||||
return null;
|
SubmitLineIntent: CallbackAction(
|
||||||
}),
|
onInvoke: (i) {
|
||||||
|
onSubmitted!(controller!.text);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
child: TypeAheadField<Map<String, String?>>(
|
child: TypeAheadField<Map<String, String?>>(
|
||||||
direction: AxisDirection.up,
|
direction: AxisDirection.up,
|
||||||
|
@ -18,23 +18,28 @@ class PinnedEvents extends StatelessWidget {
|
|||||||
const PinnedEvents(this.controller, {Key? key}) : super(key: key);
|
const PinnedEvents(this.controller, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
Future<void> _displayPinnedEventsDialog(
|
Future<void> _displayPinnedEventsDialog(
|
||||||
BuildContext context, List<Event?> events) async {
|
BuildContext context,
|
||||||
|
List<Event?> events,
|
||||||
|
) async {
|
||||||
final eventId = events.length == 1
|
final eventId = events.length == 1
|
||||||
? events.single?.eventId
|
? events.single?.eventId
|
||||||
: await showConfirmationDialog<String>(
|
: await showConfirmationDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.pinMessage,
|
title: L10n.of(context)!.pinMessage,
|
||||||
actions: events
|
actions: events
|
||||||
.map((event) => AlertDialogAction(
|
.map(
|
||||||
key: event?.eventId ?? '',
|
(event) => AlertDialogAction(
|
||||||
label: event?.calcLocalizedBodyFallback(
|
key: event?.eventId ?? '',
|
||||||
MatrixLocals(L10n.of(context)!),
|
label: event?.calcLocalizedBodyFallback(
|
||||||
withSenderNamePrefix: true,
|
MatrixLocals(L10n.of(context)!),
|
||||||
hideReply: true,
|
withSenderNamePrefix: true,
|
||||||
) ??
|
hideReply: true,
|
||||||
'UNKNOWN',
|
) ??
|
||||||
))
|
'UNKNOWN',
|
||||||
.toList());
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
if (eventId != null) controller.scrollToEventId(eventId);
|
if (eventId != null) controller.scrollToEventId(eventId);
|
||||||
}
|
}
|
||||||
@ -54,87 +59,86 @@ class PinnedEvents extends StatelessWidget {
|
|||||||
return completer;
|
return completer;
|
||||||
});
|
});
|
||||||
return FutureBuilder<List<Event?>>(
|
return FutureBuilder<List<Event?>>(
|
||||||
future: Future.wait(completers.map((e) => e.future).toList()),
|
future: Future.wait(completers.map((e) => e.future).toList()),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final pinnedEvents = snapshot.data;
|
final pinnedEvents = snapshot.data;
|
||||||
final event = (pinnedEvents != null && pinnedEvents.isNotEmpty)
|
final event = (pinnedEvents != null && pinnedEvents.isNotEmpty)
|
||||||
? snapshot.data?.last
|
? snapshot.data?.last
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (event == null || pinnedEvents == null) {
|
if (event == null || pinnedEvents == null) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _displayPinnedEventsDialog(
|
onTap: () => _displayPinnedEventsDialog(
|
||||||
context,
|
context,
|
||||||
pinnedEvents,
|
pinnedEvents,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
splashRadius: 20,
|
splashRadius: 20,
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
icon: const Icon(Icons.push_pin),
|
icon: const Icon(Icons.push_pin),
|
||||||
tooltip: L10n.of(context)!.unpin,
|
tooltip: L10n.of(context)!.unpin,
|
||||||
onPressed: controller.room
|
onPressed: controller.room
|
||||||
?.canSendEvent(EventTypes.RoomPinnedEvents) ??
|
?.canSendEvent(EventTypes.RoomPinnedEvents) ??
|
||||||
false
|
false
|
||||||
? () => controller.unpinEvent(event.eventId)
|
? () => controller.unpinEvent(event.eventId)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: FutureBuilder<String>(
|
child: FutureBuilder<String>(
|
||||||
future: event.calcLocalizedBody(
|
future: event.calcLocalizedBody(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: true,
|
withSenderNamePrefix: true,
|
||||||
hideReply: true,
|
hideReply: true,
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return LinkText(
|
||||||
|
text: snapshot.data ??
|
||||||
|
event.calcLocalizedBodyFallback(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
withSenderNamePrefix: true,
|
||||||
|
hideReply: true,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
textStyle: TextStyle(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
fontSize: fontSize,
|
||||||
|
decoration: event.redacted
|
||||||
|
? TextDecoration.lineThrough
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
linkStyle: TextStyle(
|
||||||
return LinkText(
|
color:
|
||||||
text: snapshot.data ??
|
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
event.calcLocalizedBodyFallback(
|
fontSize: fontSize,
|
||||||
MatrixLocals(L10n.of(context)!),
|
decoration: TextDecoration.underline,
|
||||||
withSenderNamePrefix: true,
|
decorationColor:
|
||||||
hideReply: true,
|
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
onLinkTap: (url) =>
|
||||||
textStyle: TextStyle(
|
UrlLauncher(context, url).launchUrl(),
|
||||||
color: Theme.of(context)
|
);
|
||||||
.colorScheme
|
},
|
||||||
.onSurfaceVariant,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
fontSize: fontSize,
|
|
||||||
decoration: event.redacted
|
|
||||||
? TextDecoration.lineThrough
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
|
||||||
fontSize: fontSize,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
decorationColor: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
onLinkTap: (url) =>
|
|
||||||
UrlLauncher(context, url).launchUrl(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,37 +26,45 @@ class ReactionsPicker extends StatelessWidget {
|
|||||||
height: (display) ? 56 : 0,
|
height: (display) ? 56 : 0,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Builder(builder: (context) {
|
child: Builder(
|
||||||
if (!display) {
|
builder: (context) {
|
||||||
return Container();
|
if (!display) {
|
||||||
}
|
return Container();
|
||||||
final proposals = proposeEmojis(
|
}
|
||||||
|
final proposals = proposeEmojis(
|
||||||
controller.selectedEvents.first.plaintextBody,
|
controller.selectedEvents.first.plaintextBody,
|
||||||
number: 25,
|
number: 25,
|
||||||
languageCodes: EmojiProposalLanguageCodes.values.toSet());
|
languageCodes: EmojiProposalLanguageCodes.values.toSet(),
|
||||||
final emojis = proposals.isNotEmpty
|
);
|
||||||
? proposals.map((e) => e.char).toList()
|
final emojis = proposals.isNotEmpty
|
||||||
: List<String>.from(AppEmojis.emojis);
|
? proposals.map((e) => e.char).toList()
|
||||||
final allReactionEvents = controller.selectedEvents.first
|
: List<String>.from(AppEmojis.emojis);
|
||||||
.aggregatedEvents(
|
final allReactionEvents = controller.selectedEvents.first
|
||||||
controller.timeline!, RelationshipTypes.reaction)
|
.aggregatedEvents(
|
||||||
.where((event) =>
|
controller.timeline!,
|
||||||
event.senderId == event.room.client.userID &&
|
RelationshipTypes.reaction,
|
||||||
event.type == 'm.reaction');
|
)
|
||||||
|
.where(
|
||||||
|
(event) =>
|
||||||
|
event.senderId == event.room.client.userID &&
|
||||||
|
event.type == 'm.reaction',
|
||||||
|
);
|
||||||
|
|
||||||
for (final event in allReactionEvents) {
|
for (final event in allReactionEvents) {
|
||||||
try {
|
try {
|
||||||
emojis.remove(event.content['m.relates_to']['key']);
|
emojis.remove(event.content['m.relates_to']['key']);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return Row(children: [
|
return Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Container(
|
Expanded(
|
||||||
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).secondaryHeaderColor,
|
color: Theme.of(context).secondaryHeaderColor,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomRight:
|
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||||
Radius.circular(AppConfig.borderRadius))),
|
),
|
||||||
|
),
|
||||||
padding: const EdgeInsets.only(right: 1),
|
padding: const EdgeInsets.only(right: 1),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
@ -74,23 +82,28 @@ class ReactionsPicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
))),
|
),
|
||||||
InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
width: 36,
|
|
||||||
height: 56,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).secondaryHeaderColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.add_outlined),
|
|
||||||
),
|
),
|
||||||
onTap: () =>
|
InkWell(
|
||||||
controller.pickEmojiReactionAction(allReactionEvents))
|
borderRadius: BorderRadius.circular(8),
|
||||||
]);
|
child: Container(
|
||||||
}),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
width: 36,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).secondaryHeaderColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.add_outlined),
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
controller.pickEmojiReactionAction(allReactionEvents),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,8 @@ class RecordingDialogState extends State<RecordingDialog> {
|
|||||||
.take(26)
|
.take(26)
|
||||||
.toList()
|
.toList()
|
||||||
.reversed
|
.reversed
|
||||||
.map((amplitude) => Container(
|
.map(
|
||||||
|
(amplitude) => Container(
|
||||||
margin: const EdgeInsets.only(left: 2),
|
margin: const EdgeInsets.only(left: 2),
|
||||||
width: 4,
|
width: 4,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -138,7 +139,9 @@ class RecordingDialogState extends State<RecordingDialog> {
|
|||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(AppConfig.borderRadius),
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
),
|
),
|
||||||
height: maxDecibalWidth * (amplitude / 100)))
|
height: maxDecibalWidth * (amplitude / 100),
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -33,10 +33,14 @@ class ReplyDisplay extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: controller.replyEvent != null
|
child: controller.replyEvent != null
|
||||||
? ReplyContent(controller.replyEvent!,
|
? ReplyContent(
|
||||||
timeline: controller.timeline!)
|
controller.replyEvent!,
|
||||||
: _EditContent(controller.editEvent
|
timeline: controller.timeline!,
|
||||||
?.getDisplayEvent(controller.timeline!)),
|
)
|
||||||
|
: _EditContent(
|
||||||
|
controller.editEvent
|
||||||
|
?.getDisplayEvent(controller.timeline!),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -64,26 +68,27 @@ class _EditContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Container(width: 15.0),
|
Container(width: 15.0),
|
||||||
FutureBuilder<String>(
|
FutureBuilder<String>(
|
||||||
future: event.calcLocalizedBody(
|
future: event.calcLocalizedBody(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: false,
|
withSenderNamePrefix: false,
|
||||||
hideReply: true,
|
hideReply: true,
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Text(
|
return Text(
|
||||||
snapshot.data ??
|
snapshot.data ??
|
||||||
event.calcLocalizedBodyFallback(
|
event.calcLocalizedBodyFallback(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: false,
|
withSenderNamePrefix: false,
|
||||||
hideReply: true,
|
hideReply: true,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -33,11 +33,12 @@ class SendFileDialogState extends State<SendFileDialog> {
|
|||||||
MatrixImageFile? thumbnail;
|
MatrixImageFile? thumbnail;
|
||||||
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
|
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
file = await file.resizeVideo();
|
file = await file.resizeVideo();
|
||||||
thumbnail = await file.getVideoThumbnail();
|
thumbnail = await file.getVideoThumbnail();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
widget.room
|
widget.room
|
||||||
@ -79,26 +80,29 @@ class SendFileDialogState extends State<SendFileDialog> {
|
|||||||
}
|
}
|
||||||
Widget contentWidget;
|
Widget contentWidget;
|
||||||
if (allFilesAreImages) {
|
if (allFilesAreImages) {
|
||||||
contentWidget = Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
contentWidget = Column(
|
||||||
Flexible(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Image.memory(
|
children: <Widget>[
|
||||||
widget.files.first.bytes,
|
Flexible(
|
||||||
fit: BoxFit.contain,
|
child: Image.memory(
|
||||||
|
widget.files.first.bytes,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(
|
||||||
Row(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Checkbox(
|
||||||
Checkbox(
|
value: origImage,
|
||||||
value: origImage,
|
onChanged: (v) => setState(() => origImage = v ?? false),
|
||||||
onChanged: (v) => setState(() => origImage = v ?? false),
|
),
|
||||||
),
|
InkWell(
|
||||||
InkWell(
|
onTap: () => setState(() => origImage = !origImage),
|
||||||
onTap: () => setState(() => origImage = !origImage),
|
child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'),
|
||||||
child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'),
|
),
|
||||||
),
|
],
|
||||||
],
|
)
|
||||||
)
|
],
|
||||||
]);
|
);
|
||||||
} else {
|
} else {
|
||||||
contentWidget = Text('$fileName ($sizeString)');
|
contentWidget = Text('$fileName ($sizeString)');
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,13 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||||||
final pack = stickerPacks[packSlugs[packIndex]]!;
|
final pack = stickerPacks[packSlugs[packIndex]]!;
|
||||||
final filteredImagePackImageEntried = pack.images.entries.toList();
|
final filteredImagePackImageEntried = pack.images.entries.toList();
|
||||||
if (searchFilter?.isNotEmpty ?? false) {
|
if (searchFilter?.isNotEmpty ?? false) {
|
||||||
filteredImagePackImageEntried.removeWhere((e) =>
|
filteredImagePackImageEntried.removeWhere(
|
||||||
!(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
|
(e) => !(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
|
||||||
(e.value.body
|
(e.value.body
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.contains(searchFilter!.toLowerCase()) ??
|
.contains(searchFilter!.toLowerCase()) ??
|
||||||
false)));
|
false)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final imageKeys =
|
final imageKeys =
|
||||||
filteredImagePackImageEntried.map((e) => e.key).toList();
|
filteredImagePackImageEntried.map((e) => e.key).toList();
|
||||||
@ -57,7 +58,8 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||||||
GridView.builder(
|
GridView.builder(
|
||||||
itemCount: imageKeys.length,
|
itemCount: imageKeys.length,
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 100),
|
maxCrossAxisExtent: 100,
|
||||||
|
),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (BuildContext context, int imageIndex) {
|
itemBuilder: (BuildContext context, int imageIndex) {
|
||||||
@ -127,10 +129,11 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
packBuilder,
|
packBuilder,
|
||||||
childCount: packSlugs.length,
|
childCount: packSlugs.length,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -58,7 +58,8 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||||||
);
|
);
|
||||||
if (success.error == null) {
|
if (success.error == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)));
|
SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,8 +213,11 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||||||
future: () => room.setDescription(input.single),
|
future: () => room.setDescription(input.single),
|
||||||
);
|
);
|
||||||
if (success.error == null) {
|
if (success.error == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged)));
|
SnackBar(
|
||||||
|
content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,7 +329,9 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||||||
void requestMoreMembersAction() async {
|
void requestMoreMembersAction() async {
|
||||||
final room = Matrix.of(context).client.getRoomById(roomId!);
|
final room = Matrix.of(context).client.getRoomById(roomId!);
|
||||||
final participants = await showFutureLoadingDialog(
|
final participants = await showFutureLoadingDialog(
|
||||||
context: context, future: () => room!.requestParticipants());
|
context: context,
|
||||||
|
future: () => room!.requestParticipants(),
|
||||||
|
);
|
||||||
if (participants.error == null) {
|
if (participants.error == null) {
|
||||||
setState(() => members = participants.result);
|
setState(() => members = participants.result);
|
||||||
}
|
}
|
||||||
|
@ -43,364 +43,400 @@ class ChatDetailsView extends StatelessWidget {
|
|||||||
controller.members!.length < actualMembersCount;
|
controller.members!.length < actualMembersCount;
|
||||||
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: room.onUpdate.stream,
|
stream: room.onUpdate.stream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder:
|
headerSliverBuilder:
|
||||||
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
|
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close_outlined),
|
icon: const Icon(Icons.close_outlined),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
VRouter.of(context).path.startsWith('/spaces/')
|
VRouter.of(context).path.startsWith('/spaces/')
|
||||||
? VRouter.of(context).pop()
|
? VRouter.of(context).pop()
|
||||||
: VRouter.of(context)
|
: VRouter.of(context)
|
||||||
.toSegments(['rooms', controller.roomId!]),
|
.toSegments(['rooms', controller.roomId!]),
|
||||||
),
|
),
|
||||||
elevation: Theme.of(context).appBarTheme.elevation,
|
elevation: Theme.of(context).appBarTheme.elevation,
|
||||||
expandedHeight: 300.0,
|
expandedHeight: 300.0,
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
if (room.canonicalAlias.isNotEmpty)
|
if (room.canonicalAlias.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: L10n.of(context)!.share,
|
tooltip: L10n.of(context)!.share,
|
||||||
icon: Icon(Icons.adaptive.share_outlined),
|
icon: Icon(Icons.adaptive.share_outlined),
|
||||||
onPressed: () => FluffyShare.share(
|
onPressed: () => FluffyShare.share(
|
||||||
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||||
context),
|
context,
|
||||||
),
|
),
|
||||||
ChatSettingsPopupMenu(room, false)
|
|
||||||
],
|
|
||||||
title: Text(
|
|
||||||
room.getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!)),
|
|
||||||
),
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).appBarTheme.backgroundColor,
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
background: ContentBanner(
|
|
||||||
mxContent: room.avatar,
|
|
||||||
onEdit: room.canSendEvent('m.room.avatar')
|
|
||||||
? controller.setAvatarAction
|
|
||||||
: null,
|
|
||||||
defaultIcon: Icons.group_outlined,
|
|
||||||
),
|
),
|
||||||
|
ChatSettingsPopupMenu(room, false)
|
||||||
|
],
|
||||||
|
title: Text(
|
||||||
|
room.getLocalizedDisplayname(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
||||||
body: MaxWidthBody(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
child: ListView.builder(
|
background: ContentBanner(
|
||||||
itemCount: controller.members!.length +
|
mxContent: room.avatar,
|
||||||
1 +
|
onEdit: room.canSendEvent('m.room.avatar')
|
||||||
(canRequestMoreMembers ? 1 : 0),
|
? controller.setAvatarAction
|
||||||
itemBuilder: (BuildContext context, int i) => i == 0
|
: null,
|
||||||
? Column(
|
defaultIcon: Icons.group_outlined,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
),
|
||||||
children: <Widget>[
|
),
|
||||||
ListTile(
|
),
|
||||||
onTap: room.canSendEvent(EventTypes.RoomTopic)
|
],
|
||||||
? controller.setTopicAction
|
body: MaxWidthBody(
|
||||||
: null,
|
child: ListView.builder(
|
||||||
trailing: room.canSendEvent(EventTypes.RoomTopic)
|
itemCount: controller.members!.length +
|
||||||
? Icon(
|
1 +
|
||||||
Icons.edit_outlined,
|
(canRequestMoreMembers ? 1 : 0),
|
||||||
color: Theme.of(context)
|
itemBuilder: (BuildContext context, int i) => i == 0
|
||||||
.colorScheme
|
? Column(
|
||||||
.onBackground,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
)
|
children: <Widget>[
|
||||||
: null,
|
ListTile(
|
||||||
title: Text(
|
onTap: room.canSendEvent(EventTypes.RoomTopic)
|
||||||
L10n.of(context)!.groupDescription,
|
? controller.setTopicAction
|
||||||
style: TextStyle(
|
: null,
|
||||||
color:
|
trailing: room.canSendEvent(EventTypes.RoomTopic)
|
||||||
Theme.of(context).colorScheme.secondary,
|
? Icon(
|
||||||
fontWeight: FontWeight.bold,
|
Icons.edit_outlined,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (room.topic.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0),
|
|
||||||
child: LinkText(
|
|
||||||
text: room.topic.isEmpty
|
|
||||||
? L10n.of(context)!.addGroupDescription
|
|
||||||
: room.topic,
|
|
||||||
linkStyle:
|
|
||||||
const TextStyle(color: Colors.blueAccent),
|
|
||||||
textStyle: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.textTheme
|
.colorScheme
|
||||||
.bodyMedium!
|
.onBackground,
|
||||||
.color,
|
)
|
||||||
decorationColor: Theme.of(context)
|
: null,
|
||||||
.textTheme
|
title: Text(
|
||||||
.bodyMedium!
|
L10n.of(context)!.groupDescription,
|
||||||
.color,
|
style: TextStyle(
|
||||||
),
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
onLinkTap: (url) =>
|
fontWeight: FontWeight.bold,
|
||||||
UrlLauncher(context, url).launchUrl(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Divider(height: 1),
|
|
||||||
ListTile(
|
|
||||||
title: Text(
|
|
||||||
L10n.of(context)!.settings,
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.secondary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Icon(controller.displaySettings
|
|
||||||
? Icons.keyboard_arrow_down_outlined
|
|
||||||
: Icons.keyboard_arrow_right_outlined),
|
|
||||||
onTap: controller.toggleDisplaySettings,
|
|
||||||
),
|
),
|
||||||
if (controller.displaySettings) ...[
|
),
|
||||||
if (room.canSendEvent('m.room.name'))
|
if (room.topic.isNotEmpty)
|
||||||
ListTile(
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
child: LinkText(
|
||||||
|
text: room.topic.isEmpty
|
||||||
|
? L10n.of(context)!.addGroupDescription
|
||||||
|
: room.topic,
|
||||||
|
linkStyle:
|
||||||
|
const TextStyle(color: Colors.blueAccent),
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.color,
|
||||||
|
decorationColor: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.color,
|
||||||
|
),
|
||||||
|
onLinkTap: (url) =>
|
||||||
|
UrlLauncher(context, url).launchUrl(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.settings,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
controller.displaySettings
|
||||||
|
? Icons.keyboard_arrow_down_outlined
|
||||||
|
: Icons.keyboard_arrow_right_outlined,
|
||||||
|
),
|
||||||
|
onTap: controller.toggleDisplaySettings,
|
||||||
|
),
|
||||||
|
if (controller.displaySettings) ...[
|
||||||
|
if (room.canSendEvent('m.room.name'))
|
||||||
|
ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
foregroundColor: iconColor,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.people_outline_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.changeTheNameOfTheGroup,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
room.getLocalizedDisplayname(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: controller.setDisplaynameAction,
|
||||||
|
),
|
||||||
|
if (room.joinRules == JoinRules.public)
|
||||||
|
ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
foregroundColor: iconColor,
|
||||||
|
child: const Icon(Icons.link_outlined),
|
||||||
|
),
|
||||||
|
onTap: controller.editAliases,
|
||||||
|
title: Text(L10n.of(context)!.editRoomAliases),
|
||||||
|
subtitle: Text(
|
||||||
|
(room.canonicalAlias.isNotEmpty)
|
||||||
|
? room.canonicalAlias
|
||||||
|
: L10n.of(context)!.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
foregroundColor: iconColor,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.insert_emoticon_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context)!.emoteSettings),
|
||||||
|
subtitle: Text(L10n.of(context)!.setCustomEmotes),
|
||||||
|
onTap: controller.goToEmoteSettings,
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
onSelected: controller.setJoinRulesAction,
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
<PopupMenuEntry<JoinRules>>[
|
||||||
|
if (room.canChangeJoinRules)
|
||||||
|
PopupMenuItem<JoinRules>(
|
||||||
|
value: JoinRules.public,
|
||||||
|
child: Text(
|
||||||
|
JoinRules.public.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (room.canChangeJoinRules)
|
||||||
|
PopupMenuItem<JoinRules>(
|
||||||
|
value: JoinRules.invite,
|
||||||
|
child: Text(
|
||||||
|
JoinRules.invite.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
foregroundColor: iconColor,
|
||||||
|
child: const Icon(Icons.shield_outlined),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.whoIsAllowedToJoinThisGroup,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
room.joinRules?.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
) ??
|
||||||
|
L10n.of(context)!.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
onSelected: controller.setHistoryVisibilityAction,
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
<PopupMenuEntry<HistoryVisibility>>[
|
||||||
|
if (room.canChangeHistoryVisibility)
|
||||||
|
PopupMenuItem<HistoryVisibility>(
|
||||||
|
value: HistoryVisibility.invited,
|
||||||
|
child: Text(
|
||||||
|
HistoryVisibility.invited
|
||||||
|
.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (room.canChangeHistoryVisibility)
|
||||||
|
PopupMenuItem<HistoryVisibility>(
|
||||||
|
value: HistoryVisibility.joined,
|
||||||
|
child: Text(
|
||||||
|
HistoryVisibility.joined
|
||||||
|
.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (room.canChangeHistoryVisibility)
|
||||||
|
PopupMenuItem<HistoryVisibility>(
|
||||||
|
value: HistoryVisibility.shared,
|
||||||
|
child: Text(
|
||||||
|
HistoryVisibility.shared
|
||||||
|
.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (room.canChangeHistoryVisibility)
|
||||||
|
PopupMenuItem<HistoryVisibility>(
|
||||||
|
value: HistoryVisibility.worldReadable,
|
||||||
|
child: Text(
|
||||||
|
HistoryVisibility.worldReadable
|
||||||
|
.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
foregroundColor: iconColor,
|
||||||
|
child: const Icon(Icons.visibility_outlined),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.visibilityOfTheChatHistory,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
room.historyVisibility?.getLocalizedString(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
) ??
|
||||||
|
L10n.of(context)!.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (room.joinRules == JoinRules.public)
|
||||||
|
PopupMenuButton(
|
||||||
|
onSelected: controller.setGuestAccessAction,
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
<PopupMenuEntry<GuestAccess>>[
|
||||||
|
if (room.canChangeGuestAccess)
|
||||||
|
PopupMenuItem<GuestAccess>(
|
||||||
|
value: GuestAccess.canJoin,
|
||||||
|
child: Text(
|
||||||
|
GuestAccess.canJoin.getLocalizedString(
|
||||||
|
MatrixLocals(
|
||||||
|
L10n.of(context)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (room.canChangeGuestAccess)
|
||||||
|
PopupMenuItem<GuestAccess>(
|
||||||
|
value: GuestAccess.forbidden,
|
||||||
|
child: Text(
|
||||||
|
GuestAccess.forbidden
|
||||||
|
.getLocalizedString(
|
||||||
|
MatrixLocals(
|
||||||
|
L10n.of(context)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: Theme.of(context)
|
backgroundColor: Theme.of(context)
|
||||||
.scaffoldBackgroundColor,
|
.scaffoldBackgroundColor,
|
||||||
foregroundColor: iconColor,
|
foregroundColor: iconColor,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.people_outline_outlined),
|
Icons.person_add_alt_1_outlined,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!
|
title: Text(
|
||||||
.changeTheNameOfTheGroup),
|
L10n.of(context)!.areGuestsAllowedToJoin,
|
||||||
subtitle: Text(room.getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
onTap: controller.setDisplaynameAction,
|
|
||||||
),
|
|
||||||
if (room.joinRules == JoinRules.public)
|
|
||||||
ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context)
|
|
||||||
.scaffoldBackgroundColor,
|
|
||||||
foregroundColor: iconColor,
|
|
||||||
child: const Icon(Icons.link_outlined),
|
|
||||||
),
|
),
|
||||||
onTap: controller.editAliases,
|
|
||||||
title:
|
|
||||||
Text(L10n.of(context)!.editRoomAliases),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
(room.canonicalAlias.isNotEmpty)
|
room.guestAccess.getLocalizedString(
|
||||||
? room.canonicalAlias
|
MatrixLocals(L10n.of(context)!),
|
||||||
: L10n.of(context)!.none),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
foregroundColor: iconColor,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.insert_emoticon_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context)!.emoteSettings),
|
|
||||||
subtitle:
|
|
||||||
Text(L10n.of(context)!.setCustomEmotes),
|
|
||||||
onTap: controller.goToEmoteSettings,
|
|
||||||
),
|
|
||||||
PopupMenuButton(
|
|
||||||
onSelected: controller.setJoinRulesAction,
|
|
||||||
itemBuilder: (BuildContext context) =>
|
|
||||||
<PopupMenuEntry<JoinRules>>[
|
|
||||||
if (room.canChangeJoinRules)
|
|
||||||
PopupMenuItem<JoinRules>(
|
|
||||||
value: JoinRules.public,
|
|
||||||
child: Text(JoinRules.public
|
|
||||||
.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
),
|
|
||||||
if (room.canChangeJoinRules)
|
|
||||||
PopupMenuItem<JoinRules>(
|
|
||||||
value: JoinRules.invite,
|
|
||||||
child: Text(JoinRules.invite
|
|
||||||
.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context)
|
|
||||||
.scaffoldBackgroundColor,
|
|
||||||
foregroundColor: iconColor,
|
|
||||||
child: const Icon(Icons.shield_outlined)),
|
|
||||||
title: Text(L10n.of(context)!
|
|
||||||
.whoIsAllowedToJoinThisGroup),
|
|
||||||
subtitle: Text(
|
|
||||||
room.joinRules?.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!)) ??
|
|
||||||
L10n.of(context)!.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuButton(
|
|
||||||
onSelected:
|
|
||||||
controller.setHistoryVisibilityAction,
|
|
||||||
itemBuilder: (BuildContext context) =>
|
|
||||||
<PopupMenuEntry<HistoryVisibility>>[
|
|
||||||
if (room.canChangeHistoryVisibility)
|
|
||||||
PopupMenuItem<HistoryVisibility>(
|
|
||||||
value: HistoryVisibility.invited,
|
|
||||||
child: Text(HistoryVisibility.invited
|
|
||||||
.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
),
|
|
||||||
if (room.canChangeHistoryVisibility)
|
|
||||||
PopupMenuItem<HistoryVisibility>(
|
|
||||||
value: HistoryVisibility.joined,
|
|
||||||
child: Text(HistoryVisibility.joined
|
|
||||||
.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
),
|
|
||||||
if (room.canChangeHistoryVisibility)
|
|
||||||
PopupMenuItem<HistoryVisibility>(
|
|
||||||
value: HistoryVisibility.shared,
|
|
||||||
child: Text(HistoryVisibility.shared
|
|
||||||
.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
),
|
|
||||||
if (room.canChangeHistoryVisibility)
|
|
||||||
PopupMenuItem<HistoryVisibility>(
|
|
||||||
value: HistoryVisibility.worldReadable,
|
|
||||||
child: Text(HistoryVisibility
|
|
||||||
.worldReadable
|
|
||||||
.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!))),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context)
|
|
||||||
.scaffoldBackgroundColor,
|
|
||||||
foregroundColor: iconColor,
|
|
||||||
child:
|
|
||||||
const Icon(Icons.visibility_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context)!
|
|
||||||
.visibilityOfTheChatHistory),
|
|
||||||
subtitle: Text(
|
|
||||||
room.historyVisibility?.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!)) ??
|
|
||||||
L10n.of(context)!.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (room.joinRules == JoinRules.public)
|
|
||||||
PopupMenuButton(
|
|
||||||
onSelected: controller.setGuestAccessAction,
|
|
||||||
itemBuilder: (BuildContext context) =>
|
|
||||||
<PopupMenuEntry<GuestAccess>>[
|
|
||||||
if (room.canChangeGuestAccess)
|
|
||||||
PopupMenuItem<GuestAccess>(
|
|
||||||
value: GuestAccess.canJoin,
|
|
||||||
child: Text(
|
|
||||||
GuestAccess.canJoin
|
|
||||||
.getLocalizedString(MatrixLocals(
|
|
||||||
L10n.of(context)!)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (room.canChangeGuestAccess)
|
|
||||||
PopupMenuItem<GuestAccess>(
|
|
||||||
value: GuestAccess.forbidden,
|
|
||||||
child: Text(
|
|
||||||
GuestAccess.forbidden
|
|
||||||
.getLocalizedString(MatrixLocals(
|
|
||||||
L10n.of(context)!)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context)
|
|
||||||
.scaffoldBackgroundColor,
|
|
||||||
foregroundColor: iconColor,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.person_add_alt_1_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context)!
|
|
||||||
.areGuestsAllowedToJoin),
|
|
||||||
subtitle: Text(
|
|
||||||
room.guestAccess.getLocalizedString(
|
|
||||||
MatrixLocals(L10n.of(context)!)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
title:
|
|
||||||
Text(L10n.of(context)!.editChatPermissions),
|
|
||||||
subtitle: Text(
|
|
||||||
L10n.of(context)!.whoCanPerformWhichAction),
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
foregroundColor: iconColor,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.edit_attributes_outlined),
|
|
||||||
),
|
|
||||||
onTap: () =>
|
|
||||||
VRouter.of(context).to('permissions'),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
const Divider(height: 1),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title:
|
||||||
actualMembersCount > 1
|
Text(L10n.of(context)!.editChatPermissions),
|
||||||
? L10n.of(context)!.countParticipants(
|
subtitle: Text(
|
||||||
actualMembersCount.toString())
|
L10n.of(context)!.whoCanPerformWhichAction,
|
||||||
: L10n.of(context)!.emptyChat,
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.secondary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
room.canInvite
|
|
||||||
? ListTile(
|
|
||||||
title:
|
|
||||||
Text(L10n.of(context)!.inviteContact),
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).primaryColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
radius: Avatar.defaultSize / 2,
|
|
||||||
child: const Icon(Icons.add_outlined),
|
|
||||||
),
|
|
||||||
onTap: () =>
|
|
||||||
VRouter.of(context).to('invite'),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: i < controller.members!.length + 1
|
|
||||||
? ParticipantListItem(controller.members![i - 1])
|
|
||||||
: ListTile(
|
|
||||||
title: Text(L10n.of(context)!
|
|
||||||
.loadCountMoreParticipants(
|
|
||||||
(actualMembersCount -
|
|
||||||
controller.members!.length)
|
|
||||||
.toString())),
|
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
foregroundColor: iconColor,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.refresh,
|
Icons.edit_attributes_outlined,
|
||||||
color: Colors.grey,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: controller.requestMoreMembersAction,
|
onTap: () =>
|
||||||
|
VRouter.of(context).to('permissions'),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
actualMembersCount > 1
|
||||||
|
? L10n.of(context)!.countParticipants(
|
||||||
|
actualMembersCount.toString(),
|
||||||
|
)
|
||||||
|
: L10n.of(context)!.emptyChat,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
room.canInvite
|
||||||
|
? ListTile(
|
||||||
|
title: Text(L10n.of(context)!.inviteContact),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
radius: Avatar.defaultSize / 2,
|
||||||
|
child: const Icon(Icons.add_outlined),
|
||||||
|
),
|
||||||
|
onTap: () => VRouter.of(context).to('invite'),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: i < controller.members!.length + 1
|
||||||
|
? ParticipantListItem(controller.members![i - 1])
|
||||||
|
: ListTile(
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.loadCountMoreParticipants(
|
||||||
|
(actualMembersCount -
|
||||||
|
controller.members!.length)
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: controller.requestMoreMembersAction,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,11 +47,12 @@ class ParticipantListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
permissionBatch,
|
permissionBatch,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -19,179 +19,180 @@ class ChatEncryptionSettingsView extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final room = controller.room;
|
final room = controller.room;
|
||||||
return StreamBuilder<Object>(
|
return StreamBuilder<Object>(
|
||||||
stream: room.client.onSync.stream.where(
|
stream: room.client.onSync.stream.where(
|
||||||
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null),
|
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null,
|
||||||
builder: (context, _) => Scaffold(
|
),
|
||||||
appBar: AppBar(
|
builder: (context, _) => Scaffold(
|
||||||
leading: IconButton(
|
appBar: AppBar(
|
||||||
icon: const Icon(Icons.close_outlined),
|
leading: IconButton(
|
||||||
onPressed: () => VRouter.of(context)
|
icon: const Icon(Icons.close_outlined),
|
||||||
.toSegments(['rooms', controller.roomId!]),
|
onPressed: () =>
|
||||||
),
|
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
|
||||||
title: Text(L10n.of(context)!.endToEndEncryption),
|
),
|
||||||
actions: [
|
title: Text(L10n.of(context)!.endToEndEncryption),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: () =>
|
TextButton(
|
||||||
launchUrlString(AppConfig.encryptionTutorial),
|
onPressed: () => launchUrlString(AppConfig.encryptionTutorial),
|
||||||
child: Text(L10n.of(context)!.help),
|
child: Text(L10n.of(context)!.help),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: CircleAvatar(
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.lock_outlined),
|
||||||
),
|
),
|
||||||
body: ListView(
|
title: Text(L10n.of(context)!.encryptThisChat),
|
||||||
children: [
|
value: room.encrypted,
|
||||||
SwitchListTile(
|
onChanged: controller.enableEncryption,
|
||||||
secondary: CircleAvatar(
|
),
|
||||||
foregroundColor:
|
Center(
|
||||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
child: Image.asset(
|
||||||
backgroundColor:
|
'assets/encryption.png',
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
width: 212,
|
||||||
child: const Icon(Icons.lock_outlined)),
|
),
|
||||||
title: Text(L10n.of(context)!.encryptThisChat),
|
),
|
||||||
value: room.encrypted,
|
const Divider(height: 1),
|
||||||
onChanged: controller.enableEncryption,
|
if (room.isDirectChat)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: controller.startVerification,
|
||||||
|
icon: const Icon(Icons.verified_outlined),
|
||||||
|
label: Text(L10n.of(context)!.verifyStart),
|
||||||
),
|
),
|
||||||
Center(
|
),
|
||||||
child: Image.asset(
|
),
|
||||||
'assets/encryption.png',
|
if (room.encrypted) ...[
|
||||||
width: 212,
|
const SizedBox(height: 16),
|
||||||
),
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.deviceKeys,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
if (room.isDirectChat)
|
),
|
||||||
Padding(
|
StreamBuilder(
|
||||||
padding: const EdgeInsets.all(16.0),
|
stream: room.onUpdate.stream,
|
||||||
child: SizedBox(
|
builder: (context, snapshot) => FutureBuilder<List<DeviceKeys>>(
|
||||||
width: double.infinity,
|
future: room.getUserDeviceKeys(),
|
||||||
child: ElevatedButton.icon(
|
builder: (BuildContext context, snapshot) {
|
||||||
onPressed: controller.startVerification,
|
if (snapshot.hasError) {
|
||||||
icon: const Icon(Icons.verified_outlined),
|
return Center(
|
||||||
label: Text(L10n.of(context)!.verifyStart),
|
child: Text(
|
||||||
|
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}',
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
if (room.encrypted) ...[
|
if (!snapshot.hasData) {
|
||||||
const SizedBox(height: 16),
|
return const Center(
|
||||||
ListTile(
|
child: CircularProgressIndicator.adaptive(
|
||||||
title: Text(
|
strokeWidth: 2,
|
||||||
L10n.of(context)!.deviceKeys,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
StreamBuilder(
|
final deviceKeys = snapshot.data!;
|
||||||
stream: room.onUpdate.stream,
|
return ListView.builder(
|
||||||
builder: (context, snapshot) => FutureBuilder<
|
shrinkWrap: true,
|
||||||
List<DeviceKeys>>(
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
future: room.getUserDeviceKeys(),
|
itemCount: deviceKeys.length,
|
||||||
builder: (BuildContext context, snapshot) {
|
itemBuilder: (BuildContext context, int i) =>
|
||||||
if (snapshot.hasError) {
|
SwitchListTile(
|
||||||
return Center(
|
value: !deviceKeys[i].blocked,
|
||||||
child: Text(
|
activeColor: deviceKeys[i].verified
|
||||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
|
? Colors.green
|
||||||
);
|
: Colors.orange,
|
||||||
}
|
onChanged: (_) =>
|
||||||
if (!snapshot.hasData) {
|
controller.toggleDeviceKey(deviceKeys[i]),
|
||||||
return const Center(
|
title: Row(
|
||||||
child: CircularProgressIndicator.adaptive(
|
children: [
|
||||||
strokeWidth: 2));
|
Icon(
|
||||||
}
|
deviceKeys[i].verified
|
||||||
final deviceKeys = snapshot.data!;
|
? Icons.verified_outlined
|
||||||
return ListView.builder(
|
: deviceKeys[i].blocked
|
||||||
shrinkWrap: true,
|
? Icons.block_outlined
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
: Icons.info_outlined,
|
||||||
itemCount: deviceKeys.length,
|
color: deviceKeys[i].verified
|
||||||
itemBuilder: (BuildContext context, int i) =>
|
? Colors.green
|
||||||
SwitchListTile(
|
: deviceKeys[i].blocked
|
||||||
value: !deviceKeys[i].blocked,
|
? Colors.red
|
||||||
activeColor: deviceKeys[i].verified
|
: Colors.orange,
|
||||||
? Colors.green
|
size: 20,
|
||||||
: Colors.orange,
|
),
|
||||||
onChanged: (_) =>
|
const SizedBox(width: 4),
|
||||||
controller.toggleDeviceKey(deviceKeys[i]),
|
Text(
|
||||||
title: Row(
|
deviceKeys[i].deviceId ??
|
||||||
children: [
|
L10n.of(context)!.unknownDevice,
|
||||||
Icon(
|
),
|
||||||
deviceKeys[i].verified
|
const SizedBox(width: 4),
|
||||||
? Icons.verified_outlined
|
Flexible(
|
||||||
: deviceKeys[i].blocked
|
fit: FlexFit.loose,
|
||||||
? Icons.block_outlined
|
child: Material(
|
||||||
: Icons.info_outlined,
|
shape: RoundedRectangleBorder(
|
||||||
color: deviceKeys[i].verified
|
borderRadius: BorderRadius.circular(
|
||||||
? Colors.green
|
AppConfig.borderRadius,
|
||||||
: deviceKeys[i].blocked
|
),
|
||||||
? Colors.red
|
side: BorderSide(
|
||||||
: Colors.orange,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
deviceKeys[i].deviceId ??
|
|
||||||
L10n.of(context)!.unknownDevice,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Flexible(
|
|
||||||
fit: FlexFit.loose,
|
|
||||||
child: Material(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
AppConfig.borderRadius),
|
|
||||||
side: BorderSide(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primaryContainer,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Text(
|
|
||||||
deviceKeys[i].userId,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
fontSize: 12,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
deviceKeys[i].ed25519Key?.beautified ??
|
|
||||||
L10n.of(context)!
|
|
||||||
.unknownEncryptionAlgorithm,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'RobotoMono',
|
|
||||||
color:
|
color:
|
||||||
Theme.of(context).colorScheme.secondary,
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Text(
|
||||||
|
deviceKeys[i].userId,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
],
|
||||||
),
|
),
|
||||||
] else
|
subtitle: Text(
|
||||||
Padding(
|
deviceKeys[i].ed25519Key?.beautified ??
|
||||||
padding: const EdgeInsets.all(16.0),
|
L10n.of(context)!.unknownEncryptionAlgorithm,
|
||||||
child: Center(
|
style: TextStyle(
|
||||||
child: Text(
|
fontFamily: 'RobotoMono',
|
||||||
L10n.of(context)!.encryptionNotEnabled,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
style: const TextStyle(
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
));
|
] else
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context)!.encryptionNotEnabled,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,19 +164,21 @@ class ChatListController extends State<ChatList>
|
|||||||
|
|
||||||
void setServer() async {
|
void setServer() async {
|
||||||
final newServer = await showTextInputDialog(
|
final newServer = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
title: L10n.of(context)!.changeTheHomeserver,
|
title: L10n.of(context)!.changeTheHomeserver,
|
||||||
context: context,
|
context: context,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
textFields: [
|
textFields: [
|
||||||
DialogTextField(
|
DialogTextField(
|
||||||
prefixText: 'https://',
|
prefixText: 'https://',
|
||||||
hintText: Matrix.of(context).client.homeserver?.host,
|
hintText: Matrix.of(context).client.homeserver?.host,
|
||||||
initialText: searchServer,
|
initialText: searchServer,
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
autocorrect: false)
|
autocorrect: false,
|
||||||
]);
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
if (newServer == null) return;
|
if (newServer == null) return;
|
||||||
Store().setItem(_serverStoreNamespace, newServer.single);
|
Store().setItem(_serverStoreNamespace, newServer.single);
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -382,9 +384,11 @@ class ChatListController extends State<ChatList>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void toggleSelection(String roomId) {
|
void toggleSelection(String roomId) {
|
||||||
setState(() => selectedRoomIds.contains(roomId)
|
setState(
|
||||||
? selectedRoomIds.remove(roomId)
|
() => selectedRoomIds.contains(roomId)
|
||||||
: selectedRoomIds.add(roomId));
|
? selectedRoomIds.remove(roomId)
|
||||||
|
: selectedRoomIds.add(roomId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleUnread() async {
|
Future<void> toggleUnread() async {
|
||||||
@ -456,16 +460,17 @@ class ChatListController extends State<ChatList>
|
|||||||
|
|
||||||
void setStatus() async {
|
void setStatus() async {
|
||||||
final input = await showTextInputDialog(
|
final input = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.setStatus,
|
title: L10n.of(context)!.setStatus,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
textFields: [
|
textFields: [
|
||||||
DialogTextField(
|
DialogTextField(
|
||||||
hintText: L10n.of(context)!.statusExampleMessage,
|
hintText: L10n.of(context)!.statusExampleMessage,
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
if (input == null) return;
|
if (input == null) return;
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -491,22 +496,23 @@ class ChatListController extends State<ChatList>
|
|||||||
|
|
||||||
Future<void> addToSpace() async {
|
Future<void> addToSpace() async {
|
||||||
final selectedSpace = await showConfirmationDialog<String>(
|
final selectedSpace = await showConfirmationDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.addToSpace,
|
title: L10n.of(context)!.addToSpace,
|
||||||
message: L10n.of(context)!.addToSpaceDescription,
|
message: L10n.of(context)!.addToSpaceDescription,
|
||||||
fullyCapitalizedForMaterial: false,
|
fullyCapitalizedForMaterial: false,
|
||||||
actions: Matrix.of(context)
|
actions: Matrix.of(context)
|
||||||
.client
|
.client
|
||||||
.rooms
|
.rooms
|
||||||
.where((r) => r.isSpace)
|
.where((r) => r.isSpace)
|
||||||
.map(
|
.map(
|
||||||
(space) => AlertDialogAction(
|
(space) => AlertDialogAction(
|
||||||
key: space.id,
|
key: space.id,
|
||||||
label: space
|
label: space
|
||||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList());
|
.toList(),
|
||||||
|
);
|
||||||
if (selectedSpace == null) return;
|
if (selectedSpace == null) return;
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -532,14 +538,19 @@ class ChatListController extends State<ChatList>
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any(
|
bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any(
|
||||||
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.markedUnread);
|
(roomId) =>
|
||||||
|
!Matrix.of(context).client.getRoomById(roomId)!.markedUnread,
|
||||||
|
);
|
||||||
|
|
||||||
bool get anySelectedRoomNotFavorite => selectedRoomIds.any(
|
bool get anySelectedRoomNotFavorite => selectedRoomIds.any(
|
||||||
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite);
|
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite,
|
||||||
|
);
|
||||||
|
|
||||||
bool get anySelectedRoomNotMuted => selectedRoomIds.any((roomId) =>
|
bool get anySelectedRoomNotMuted => selectedRoomIds.any(
|
||||||
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
|
(roomId) =>
|
||||||
PushRuleState.notify);
|
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
|
||||||
|
PushRuleState.notify,
|
||||||
|
);
|
||||||
|
|
||||||
bool waitForFirstSync = false;
|
bool waitForFirstSync = false;
|
||||||
|
|
||||||
@ -624,9 +635,10 @@ class ChatListController extends State<ChatList>
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case EditBundleAction.addToBundle:
|
case EditBundleAction.addToBundle:
|
||||||
final bundle = await showTextInputDialog(
|
final bundle = await showTextInputDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: l10n.bundleName,
|
title: l10n.bundleName,
|
||||||
textFields: [DialogTextField(hintText: l10n.bundleName)]);
|
textFields: [DialogTextField(hintText: l10n.bundleName)],
|
||||||
|
);
|
||||||
if (bundle == null || bundle.isEmpty || bundle.single.isEmpty) return;
|
if (bundle == null || bundle.isEmpty || bundle.single.isEmpty) return;
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -46,247 +46,248 @@ class ChatListViewBody extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: StreamBuilder(
|
child: StreamBuilder(
|
||||||
key: ValueKey(client.userID.toString() +
|
key: ValueKey(
|
||||||
|
client.userID.toString() +
|
||||||
controller.activeFilter.toString() +
|
controller.activeFilter.toString() +
|
||||||
controller.activeSpaceId.toString()),
|
controller.activeSpaceId.toString(),
|
||||||
stream: client.onSync.stream
|
),
|
||||||
.where((s) => s.hasRoomUpdate)
|
stream: client.onSync.stream
|
||||||
.rateLimit(const Duration(seconds: 1)),
|
.where((s) => s.hasRoomUpdate)
|
||||||
builder: (context, _) {
|
.rateLimit(const Duration(seconds: 1)),
|
||||||
if (controller.activeFilter == ActiveFilter.spaces &&
|
builder: (context, _) {
|
||||||
!controller.isSearchMode) {
|
if (controller.activeFilter == ActiveFilter.spaces &&
|
||||||
return SpaceView(
|
!controller.isSearchMode) {
|
||||||
controller,
|
return SpaceView(
|
||||||
scrollController: controller.scrollController,
|
controller,
|
||||||
key: Key(controller.activeSpaceId ?? 'Spaces'),
|
scrollController: controller.scrollController,
|
||||||
);
|
key: Key(controller.activeSpaceId ?? 'Spaces'),
|
||||||
}
|
);
|
||||||
if (controller.waitForFirstSync && client.prevBatch != null) {
|
}
|
||||||
final rooms = controller.filteredRooms;
|
if (controller.waitForFirstSync && client.prevBatch != null) {
|
||||||
final displayStoriesHeader = {
|
final rooms = controller.filteredRooms;
|
||||||
ActiveFilter.allChats,
|
final displayStoriesHeader = {
|
||||||
ActiveFilter.messages,
|
ActiveFilter.allChats,
|
||||||
}.contains(controller.activeFilter) &&
|
ActiveFilter.messages,
|
||||||
client.storiesRooms.isNotEmpty;
|
}.contains(controller.activeFilter) &&
|
||||||
return ListView.builder(
|
client.storiesRooms.isNotEmpty;
|
||||||
controller: controller.scrollController,
|
return ListView.builder(
|
||||||
// add +1 space below in order to properly scroll below the spaces bar
|
controller: controller.scrollController,
|
||||||
itemCount: rooms.length + 1,
|
// add +1 space below in order to properly scroll below the spaces bar
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemCount: rooms.length + 1,
|
||||||
if (i == 0) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
return Column(
|
if (i == 0) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (controller.isSearchMode) ...[
|
children: [
|
||||||
SearchTitle(
|
if (controller.isSearchMode) ...[
|
||||||
title: L10n.of(context)!.publicRooms,
|
SearchTitle(
|
||||||
icon: const Icon(Icons.explore_outlined),
|
title: L10n.of(context)!.publicRooms,
|
||||||
),
|
icon: const Icon(Icons.explore_outlined),
|
||||||
AnimatedContainer(
|
),
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
decoration: const BoxDecoration(),
|
|
||||||
height: roomSearchResult == null ||
|
|
||||||
roomSearchResult.chunk.isEmpty
|
|
||||||
? 0
|
|
||||||
: 106,
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
child: roomSearchResult == null
|
|
||||||
? null
|
|
||||||
: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: roomSearchResult.chunk.length,
|
|
||||||
itemBuilder: (context, i) => _SearchItem(
|
|
||||||
title: roomSearchResult.chunk[i].name ??
|
|
||||||
roomSearchResult.chunk[i]
|
|
||||||
.canonicalAlias?.localpart ??
|
|
||||||
L10n.of(context)!.group,
|
|
||||||
avatar:
|
|
||||||
roomSearchResult.chunk[i].avatarUrl,
|
|
||||||
onPressed: () => showAdaptiveBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (c) => PublicRoomBottomSheet(
|
|
||||||
roomAlias: roomSearchResult
|
|
||||||
.chunk[i].canonicalAlias ??
|
|
||||||
roomSearchResult.chunk[i].roomId,
|
|
||||||
outerContext: context,
|
|
||||||
chunk: roomSearchResult.chunk[i],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SearchTitle(
|
|
||||||
title: L10n.of(context)!.users,
|
|
||||||
icon: const Icon(Icons.group_outlined),
|
|
||||||
),
|
|
||||||
AnimatedContainer(
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
decoration: const BoxDecoration(),
|
|
||||||
height: userSearchResult == null ||
|
|
||||||
userSearchResult.results.isEmpty
|
|
||||||
? 0
|
|
||||||
: 106,
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
child: userSearchResult == null
|
|
||||||
? null
|
|
||||||
: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: userSearchResult.results.length,
|
|
||||||
itemBuilder: (context, i) => _SearchItem(
|
|
||||||
title: userSearchResult
|
|
||||||
.results[i].displayName ??
|
|
||||||
userSearchResult
|
|
||||||
.results[i].userId.localpart ??
|
|
||||||
L10n.of(context)!.unknownDevice,
|
|
||||||
avatar:
|
|
||||||
userSearchResult.results[i].avatarUrl,
|
|
||||||
onPressed: () => showAdaptiveBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (c) => ProfileBottomSheet(
|
|
||||||
userId: userSearchResult
|
|
||||||
.results[i].userId,
|
|
||||||
outerContext: context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SearchTitle(
|
|
||||||
title: L10n.of(context)!.stories,
|
|
||||||
icon: const Icon(Icons.camera_alt_outlined),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (displayStoriesHeader)
|
|
||||||
StoriesHeader(
|
|
||||||
key: const Key('stories_header'),
|
|
||||||
filter: controller.searchController.text,
|
|
||||||
),
|
|
||||||
const ConnectionStatusHeader(),
|
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
height: controller.isTorBrowser ? 64 : 0,
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: const BoxDecoration(),
|
decoration: const BoxDecoration(),
|
||||||
child: Material(
|
height: roomSearchResult == null ||
|
||||||
color: Theme.of(context).colorScheme.surface,
|
roomSearchResult.chunk.isEmpty
|
||||||
child: ListTile(
|
? 0
|
||||||
leading: const Icon(Icons.vpn_key),
|
: 106,
|
||||||
title: Text(L10n.of(context)!.dehydrateTor),
|
duration: FluffyThemes.animationDuration,
|
||||||
subtitle:
|
curve: FluffyThemes.animationCurve,
|
||||||
Text(L10n.of(context)!.dehydrateTorLong),
|
child: roomSearchResult == null
|
||||||
trailing:
|
? null
|
||||||
const Icon(Icons.chevron_right_outlined),
|
: ListView.builder(
|
||||||
onTap: controller.dehydrate,
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
itemCount: roomSearchResult.chunk.length,
|
||||||
),
|
itemBuilder: (context, i) => _SearchItem(
|
||||||
),
|
title: roomSearchResult.chunk[i].name ??
|
||||||
if (controller.isSearchMode)
|
roomSearchResult.chunk[i].canonicalAlias
|
||||||
SearchTitle(
|
?.localpart ??
|
||||||
title: L10n.of(context)!.chats,
|
L10n.of(context)!.group,
|
||||||
icon: const Icon(Icons.chat_outlined),
|
avatar: roomSearchResult.chunk[i].avatarUrl,
|
||||||
),
|
onPressed: () => showAdaptiveBottomSheet(
|
||||||
if (rooms.isEmpty && !controller.isSearchMode)
|
context: context,
|
||||||
Padding(
|
builder: (c) => PublicRoomBottomSheet(
|
||||||
padding: const EdgeInsets.all(32.0),
|
roomAlias: roomSearchResult
|
||||||
child: Column(
|
.chunk[i].canonicalAlias ??
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
roomSearchResult.chunk[i].roomId,
|
||||||
children: [
|
outerContext: context,
|
||||||
Image.asset(
|
chunk: roomSearchResult.chunk[i],
|
||||||
'assets/start_chat.png',
|
),
|
||||||
height: 256,
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
],
|
SearchTitle(
|
||||||
),
|
title: L10n.of(context)!.users,
|
||||||
),
|
icon: const Icon(Icons.group_outlined),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: const BoxDecoration(),
|
||||||
|
height: userSearchResult == null ||
|
||||||
|
userSearchResult.results.isEmpty
|
||||||
|
? 0
|
||||||
|
: 106,
|
||||||
|
duration: FluffyThemes.animationDuration,
|
||||||
|
curve: FluffyThemes.animationCurve,
|
||||||
|
child: userSearchResult == null
|
||||||
|
? null
|
||||||
|
: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: userSearchResult.results.length,
|
||||||
|
itemBuilder: (context, i) => _SearchItem(
|
||||||
|
title: userSearchResult
|
||||||
|
.results[i].displayName ??
|
||||||
|
userSearchResult
|
||||||
|
.results[i].userId.localpart ??
|
||||||
|
L10n.of(context)!.unknownDevice,
|
||||||
|
avatar:
|
||||||
|
userSearchResult.results[i].avatarUrl,
|
||||||
|
onPressed: () => showAdaptiveBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => ProfileBottomSheet(
|
||||||
|
userId:
|
||||||
|
userSearchResult.results[i].userId,
|
||||||
|
outerContext: context,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SearchTitle(
|
||||||
|
title: L10n.of(context)!.stories,
|
||||||
|
icon: const Icon(Icons.camera_alt_outlined),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
if (displayStoriesHeader)
|
||||||
}
|
StoriesHeader(
|
||||||
i--;
|
key: const Key('stories_header'),
|
||||||
if (!rooms[i]
|
filter: controller.searchController.text,
|
||||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
),
|
||||||
.toLowerCase()
|
const ConnectionStatusHeader(),
|
||||||
.contains(
|
AnimatedContainer(
|
||||||
controller.searchController.text.toLowerCase())) {
|
height: controller.isTorBrowser ? 64 : 0,
|
||||||
return Container();
|
duration: FluffyThemes.animationDuration,
|
||||||
}
|
curve: FluffyThemes.animationCurve,
|
||||||
return ChatListItem(
|
clipBehavior: Clip.hardEdge,
|
||||||
rooms[i],
|
decoration: const BoxDecoration(),
|
||||||
key: Key('chat_list_item_${rooms[i].id}'),
|
child: Material(
|
||||||
selected: controller.selectedRoomIds.contains(rooms[i].id),
|
color: Theme.of(context).colorScheme.surface,
|
||||||
onTap: controller.selectMode == SelectMode.select
|
child: ListTile(
|
||||||
? () => controller.toggleSelection(rooms[i].id)
|
leading: const Icon(Icons.vpn_key),
|
||||||
: null,
|
title: Text(L10n.of(context)!.dehydrateTor),
|
||||||
onLongPress: () => controller.toggleSelection(rooms[i].id),
|
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
|
||||||
activeChat: controller.activeChat == rooms[i].id,
|
trailing: const Icon(Icons.chevron_right_outlined),
|
||||||
);
|
onTap: controller.dehydrate,
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const dummyChatCount = 5;
|
|
||||||
final titleColor =
|
|
||||||
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
|
|
||||||
final subtitleColor =
|
|
||||||
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
|
|
||||||
return ListView.builder(
|
|
||||||
key: const Key('dummychats'),
|
|
||||||
itemCount: dummyChatCount,
|
|
||||||
itemBuilder: (context, i) => Opacity(
|
|
||||||
opacity: (dummyChatCount - i) / dummyChatCount,
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: titleColor,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 1,
|
|
||||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
height: 14,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: titleColor,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 36),
|
if (controller.isSearchMode)
|
||||||
Container(
|
SearchTitle(
|
||||||
height: 14,
|
title: L10n.of(context)!.chats,
|
||||||
width: 14,
|
icon: const Icon(Icons.chat_outlined),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: subtitleColor,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
),
|
||||||
),
|
if (rooms.isEmpty && !controller.isSearchMode)
|
||||||
const SizedBox(width: 12),
|
Padding(
|
||||||
Container(
|
padding: const EdgeInsets.all(32.0),
|
||||||
height: 14,
|
child: Column(
|
||||||
width: 14,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: subtitleColor,
|
Image.asset(
|
||||||
borderRadius: BorderRadius.circular(14),
|
'assets/start_chat.png',
|
||||||
|
height: 256,
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
subtitle: Container(
|
}
|
||||||
decoration: BoxDecoration(
|
i--;
|
||||||
color: subtitleColor,
|
if (!rooms[i]
|
||||||
borderRadius: BorderRadius.circular(3),
|
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
||||||
),
|
.toLowerCase()
|
||||||
height: 12,
|
.contains(
|
||||||
margin: const EdgeInsets.only(right: 22),
|
controller.searchController.text.toLowerCase(),
|
||||||
|
)) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
return ChatListItem(
|
||||||
|
rooms[i],
|
||||||
|
key: Key('chat_list_item_${rooms[i].id}'),
|
||||||
|
selected: controller.selectedRoomIds.contains(rooms[i].id),
|
||||||
|
onTap: controller.selectMode == SelectMode.select
|
||||||
|
? () => controller.toggleSelection(rooms[i].id)
|
||||||
|
: null,
|
||||||
|
onLongPress: () => controller.toggleSelection(rooms[i].id),
|
||||||
|
activeChat: controller.activeChat == rooms[i].id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const dummyChatCount = 5;
|
||||||
|
final titleColor =
|
||||||
|
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
|
||||||
|
final subtitleColor =
|
||||||
|
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
|
||||||
|
return ListView.builder(
|
||||||
|
key: const Key('dummychats'),
|
||||||
|
itemCount: dummyChatCount,
|
||||||
|
itemBuilder: (context, i) => Opacity(
|
||||||
|
opacity: (dummyChatCount - i) / dummyChatCount,
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: titleColor,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1,
|
||||||
|
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 36),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: subtitleColor,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: subtitleColor,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: subtitleColor,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.only(right: 22),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,22 +120,28 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: L10n.of(context)!.toggleUnread,
|
tooltip: L10n.of(context)!.toggleUnread,
|
||||||
icon: Icon(controller.anySelectedRoomNotMarkedUnread
|
icon: Icon(
|
||||||
? Icons.mark_chat_read_outlined
|
controller.anySelectedRoomNotMarkedUnread
|
||||||
: Icons.mark_chat_unread_outlined),
|
? Icons.mark_chat_read_outlined
|
||||||
|
: Icons.mark_chat_unread_outlined,
|
||||||
|
),
|
||||||
onPressed: controller.toggleUnread,
|
onPressed: controller.toggleUnread,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: L10n.of(context)!.toggleFavorite,
|
tooltip: L10n.of(context)!.toggleFavorite,
|
||||||
icon: Icon(controller.anySelectedRoomNotFavorite
|
icon: Icon(
|
||||||
? Icons.push_pin_outlined
|
controller.anySelectedRoomNotFavorite
|
||||||
: Icons.push_pin),
|
? Icons.push_pin_outlined
|
||||||
|
: Icons.push_pin,
|
||||||
|
),
|
||||||
onPressed: controller.toggleFavouriteRoom,
|
onPressed: controller.toggleFavouriteRoom,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(controller.anySelectedRoomNotMuted
|
icon: Icon(
|
||||||
? Icons.notifications_off_outlined
|
controller.anySelectedRoomNotMuted
|
||||||
: Icons.notifications_outlined),
|
? Icons.notifications_off_outlined
|
||||||
|
: Icons.notifications_outlined,
|
||||||
|
),
|
||||||
tooltip: L10n.of(context)!.toggleMuted,
|
tooltip: L10n.of(context)!.toggleMuted,
|
||||||
onPressed: controller.toggleMuted,
|
onPressed: controller.toggleMuted,
|
||||||
),
|
),
|
||||||
|
@ -38,15 +38,16 @@ class ChatListItem extends StatelessWidget {
|
|||||||
if (activeChat) return;
|
if (activeChat) return;
|
||||||
if (room.membership == Membership.invite) {
|
if (room.membership == Membership.invite) {
|
||||||
final joinResult = await showFutureLoadingDialog(
|
final joinResult = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
final waitForRoom = room.client.waitForRoomInSync(
|
final waitForRoom = room.client.waitForRoomInSync(
|
||||||
room.id,
|
room.id,
|
||||||
join: true,
|
join: true,
|
||||||
);
|
);
|
||||||
await room.join();
|
await room.join();
|
||||||
await waitForRoom;
|
await waitForRoom;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (joinResult.error != null) return;
|
if (joinResult.error != null) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +108,9 @@ class ChatListItem extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (confirmed == OkCancelResult.cancel) return;
|
if (confirmed == OkCancelResult.cancel) return;
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context, future: () => room.leave());
|
context: context,
|
||||||
|
future: () => room.leave(),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +186,8 @@ class ChatListItem extends StatelessWidget {
|
|||||||
if (room.isFavourite)
|
if (room.isFavourite)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: room.notificationCount > 0 ? 4.0 : 0.0),
|
right: room.notificationCount > 0 ? 4.0 : 0.0,
|
||||||
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.push_pin,
|
Icons.push_pin,
|
||||||
size: 16,
|
size: 16,
|
||||||
@ -282,7 +286,8 @@ class ChatListItem extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
|
@ -109,56 +109,60 @@ class ChatListView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (FluffyThemes.isColumnMode(context) &&
|
if (FluffyThemes.isColumnMode(context) &&
|
||||||
FluffyThemes.getDisplayNavigationRail(context)) ...[
|
FluffyThemes.getDisplayNavigationRail(context)) ...[
|
||||||
Builder(builder: (context) {
|
Builder(
|
||||||
final allSpaces = client.rooms.where((room) => room.isSpace);
|
builder: (context) {
|
||||||
final rootSpaces = allSpaces
|
final allSpaces =
|
||||||
.where(
|
client.rooms.where((room) => room.isSpace);
|
||||||
(space) => !allSpaces.any(
|
final rootSpaces = allSpaces
|
||||||
(parentSpace) => parentSpace.spaceChildren
|
.where(
|
||||||
.any((child) => child.roomId == space.id),
|
(space) => !allSpaces.any(
|
||||||
),
|
(parentSpace) => parentSpace.spaceChildren
|
||||||
)
|
.any((child) => child.roomId == space.id),
|
||||||
.toList();
|
),
|
||||||
final destinations = getNavigationDestinations(context);
|
)
|
||||||
|
.toList();
|
||||||
|
final destinations = getNavigationDestinations(context);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 64,
|
width: 64,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
itemCount: rootSpaces.length + destinations.length,
|
itemCount: rootSpaces.length + destinations.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i < destinations.length) {
|
if (i < destinations.length) {
|
||||||
|
return NaviRailItem(
|
||||||
|
isSelected: i == controller.selectedIndex,
|
||||||
|
onTap: () => controller.onDestinationSelected(i),
|
||||||
|
icon: destinations[i].icon,
|
||||||
|
selectedIcon: destinations[i].selectedIcon,
|
||||||
|
toolTip: destinations[i].label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
i -= destinations.length;
|
||||||
|
final isSelected =
|
||||||
|
controller.activeFilter == ActiveFilter.spaces &&
|
||||||
|
rootSpaces[i].id == controller.activeSpaceId;
|
||||||
return NaviRailItem(
|
return NaviRailItem(
|
||||||
isSelected: i == controller.selectedIndex,
|
toolTip: rootSpaces[i].getLocalizedDisplayname(
|
||||||
onTap: () => controller.onDestinationSelected(i),
|
|
||||||
icon: destinations[i].icon,
|
|
||||||
selectedIcon: destinations[i].selectedIcon,
|
|
||||||
toolTip: destinations[i].label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
i -= destinations.length;
|
|
||||||
final isSelected =
|
|
||||||
controller.activeFilter == ActiveFilter.spaces &&
|
|
||||||
rootSpaces[i].id == controller.activeSpaceId;
|
|
||||||
return NaviRailItem(
|
|
||||||
toolTip: rootSpaces[i].getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!)),
|
|
||||||
isSelected: isSelected,
|
|
||||||
onTap: () =>
|
|
||||||
controller.setActiveSpace(rootSpaces[i].id),
|
|
||||||
icon: Avatar(
|
|
||||||
mxContent: rootSpaces[i].avatar,
|
|
||||||
name: rootSpaces[i].getLocalizedDisplayname(
|
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
),
|
),
|
||||||
size: 32,
|
isSelected: isSelected,
|
||||||
fontSize: 12,
|
onTap: () =>
|
||||||
),
|
controller.setActiveSpace(rootSpaces[i].id),
|
||||||
);
|
icon: Avatar(
|
||||||
},
|
mxContent: rootSpaces[i].avatar,
|
||||||
),
|
name: rootSpaces[i].getLocalizedDisplayname(
|
||||||
);
|
MatrixLocals(L10n.of(context)!),
|
||||||
}),
|
),
|
||||||
|
size: 32,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
|
@ -20,11 +20,13 @@ class ClientChooserButton extends StatelessWidget {
|
|||||||
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
||||||
final matrix = Matrix.of(context);
|
final matrix = Matrix.of(context);
|
||||||
final bundles = matrix.accountBundles.keys.toList()
|
final bundles = matrix.accountBundles.keys.toList()
|
||||||
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
..sort(
|
||||||
? 0
|
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||||
: a.isValidMatrixId && !b.isValidMatrixId
|
? 0
|
||||||
? -1
|
: a.isValidMatrixId && !b.isValidMatrixId
|
||||||
: 1);
|
? -1
|
||||||
|
: 1,
|
||||||
|
);
|
||||||
return <PopupMenuEntry<Object>>[
|
return <PopupMenuEntry<Object>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: SettingsAction.newStory,
|
value: SettingsAction.newStory,
|
||||||
@ -142,7 +144,9 @@ class ClientChooserButton extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
onPressed: () => controller.editBundlesForAccount(
|
onPressed: () => controller.editBundlesForAccount(
|
||||||
client.userID, bundle),
|
client.userID,
|
||||||
|
bundle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -270,9 +274,12 @@ class ClientChooserButton extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
case SettingsAction.invite:
|
case SettingsAction.invite:
|
||||||
FluffyShare.share(
|
FluffyShare.share(
|
||||||
L10n.of(context)!.inviteText(Matrix.of(context).client.userID!,
|
L10n.of(context)!.inviteText(
|
||||||
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'),
|
Matrix.of(context).client.userID!,
|
||||||
context);
|
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat',
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case SettingsAction.settings:
|
case SettingsAction.settings:
|
||||||
VRouter.of(context).to('/settings');
|
VRouter.of(context).to('/settings');
|
||||||
@ -290,11 +297,13 @@ class ClientChooserButton extends StatelessWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
final bundles = matrix.accountBundles.keys.toList()
|
final bundles = matrix.accountBundles.keys.toList()
|
||||||
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
..sort(
|
||||||
? 0
|
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||||
: a.isValidMatrixId && !b.isValidMatrixId
|
? 0
|
||||||
? -1
|
: a.isValidMatrixId && !b.isValidMatrixId
|
||||||
: 1);
|
? -1
|
||||||
|
: 1,
|
||||||
|
);
|
||||||
// beginning from end if negative
|
// beginning from end if negative
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
int clientCount = 0;
|
int clientCount = 0;
|
||||||
@ -320,11 +329,13 @@ class ClientChooserButton extends StatelessWidget {
|
|||||||
int index = 0;
|
int index = 0;
|
||||||
|
|
||||||
final bundles = matrix.accountBundles.keys.toList()
|
final bundles = matrix.accountBundles.keys.toList()
|
||||||
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
..sort(
|
||||||
? 0
|
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||||
: a.isValidMatrixId && !b.isValidMatrixId
|
? 0
|
||||||
? -1
|
: a.isValidMatrixId && !b.isValidMatrixId
|
||||||
: 1);
|
? -1
|
||||||
|
: 1,
|
||||||
|
);
|
||||||
for (final bundleName in bundles) {
|
for (final bundleName in bundles) {
|
||||||
final bundle = matrix.accountBundles[bundleName];
|
final bundle = matrix.accountBundles[bundleName];
|
||||||
if (bundle == null) return null;
|
if (bundle == null) return null;
|
||||||
|
@ -48,17 +48,18 @@ class NaviRailItem extends StatelessWidget {
|
|||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
tooltip: toolTip,
|
tooltip: toolTip,
|
||||||
icon: Material(
|
icon: Material(
|
||||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).colorScheme.primaryContainer
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
: Theme.of(context).colorScheme.background,
|
: Theme.of(context).colorScheme.background,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8.0,
|
horizontal: 8.0,
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
),
|
),
|
||||||
child: isSelected ? selectedIcon ?? icon : icon,
|
child: isSelected ? selectedIcon ?? icon : icon,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -45,13 +45,15 @@ class SearchTitle extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
icon,
|
icon,
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(title,
|
Text(
|
||||||
textAlign: TextAlign.left,
|
title,
|
||||||
style: TextStyle(
|
textAlign: TextAlign.left,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
style: TextStyle(
|
||||||
fontSize: 12,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 12,
|
||||||
)),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (trailing != null)
|
if (trailing != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
|
@ -53,11 +53,14 @@ class _SpaceViewState extends State<SpaceView> {
|
|||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
await client.joinRoom(spaceChild.roomId,
|
await client.joinRoom(
|
||||||
serverName: space?.spaceChildren
|
spaceChild.roomId,
|
||||||
.firstWhereOrNull(
|
serverName: space?.spaceChildren
|
||||||
(child) => child.roomId == spaceChild.roomId)
|
.firstWhereOrNull(
|
||||||
?.via);
|
(child) => child.roomId == spaceChild.roomId,
|
||||||
|
)
|
||||||
|
?.via,
|
||||||
|
);
|
||||||
if (client.getRoomById(spaceChild.roomId) == null) {
|
if (client.getRoomById(spaceChild.roomId) == null) {
|
||||||
// Wait for room actually appears in sync
|
// Wait for room actually appears in sync
|
||||||
await client.waitForRoomInSync(spaceChild.roomId, join: true);
|
await client.waitForRoomInSync(spaceChild.roomId, join: true);
|
||||||
@ -78,8 +81,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||||||
VRouter.of(context).toSegments(['rooms', spaceChild.roomId]);
|
VRouter.of(context).toSegments(['rooms', spaceChild.roomId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSpaceChildContextMenu(
|
void _onSpaceChildContextMenu([
|
||||||
[SpaceRoomsChunk? spaceChild, Room? room]) async {
|
SpaceRoomsChunk? spaceChild,
|
||||||
|
Room? room,
|
||||||
|
]) async {
|
||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
final activeSpaceId = widget.controller.activeSpaceId;
|
final activeSpaceId = widget.controller.activeSpaceId;
|
||||||
final activeSpace =
|
final activeSpace =
|
||||||
@ -169,8 +174,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: Text(L10n.of(context)!
|
subtitle: Text(
|
||||||
.numChats(rootSpace.spaceChildren.length.toString())),
|
L10n.of(context)!
|
||||||
|
.numChats(rootSpace.spaceChildren.length.toString()),
|
||||||
|
),
|
||||||
onTap: () => widget.controller.setActiveSpace(rootSpace.id),
|
onTap: () => widget.controller.setActiveSpace(rootSpace.id),
|
||||||
onLongPress: () => _onSpaceChildContextMenu(null, rootSpace),
|
onLongPress: () => _onSpaceChildContextMenu(null, rootSpace),
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
trailing: const Icon(Icons.chevron_right_outlined),
|
||||||
@ -180,166 +187,166 @@ class _SpaceViewState extends State<SpaceView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return FutureBuilder<GetSpaceHierarchyResponse>(
|
return FutureBuilder<GetSpaceHierarchyResponse>(
|
||||||
future: getFuture(activeSpaceId),
|
future: getFuture(activeSpaceId),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final response = snapshot.data;
|
final response = snapshot.data;
|
||||||
final error = snapshot.error;
|
final error = snapshot.error;
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(error.toLocalizedString(context)),
|
child: Text(error.toLocalizedString(context)),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
icon: const Icon(Icons.refresh_outlined),
|
icon: const Icon(Icons.refresh_outlined),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
return const Center(child: CircularProgressIndicator.adaptive());
|
return const Center(child: CircularProgressIndicator.adaptive());
|
||||||
}
|
}
|
||||||
final parentSpace = allSpaces.firstWhereOrNull((space) => space
|
final parentSpace = allSpaces.firstWhereOrNull(
|
||||||
.spaceChildren
|
(space) =>
|
||||||
.any((child) => child.roomId == activeSpaceId));
|
space.spaceChildren.any((child) => child.roomId == activeSpaceId),
|
||||||
final spaceChildren = response.rooms;
|
);
|
||||||
final canLoadMore = response.nextBatch != null;
|
final spaceChildren = response.rooms;
|
||||||
return VWidgetGuard(
|
final canLoadMore = response.nextBatch != null;
|
||||||
onSystemPop: (redirector) async {
|
return VWidgetGuard(
|
||||||
if (parentSpace != null) {
|
onSystemPop: (redirector) async {
|
||||||
widget.controller.setActiveSpace(parentSpace.id);
|
if (parentSpace != null) {
|
||||||
redirector.stopRedirection();
|
widget.controller.setActiveSpace(parentSpace.id);
|
||||||
return;
|
redirector.stopRedirection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: spaceChildren.length + 1 + (canLoadMore ? 1 : 0),
|
||||||
|
controller: widget.scrollController,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
if (i == 0) {
|
||||||
|
return ListTile(
|
||||||
|
leading: BackButton(
|
||||||
|
onPressed: () =>
|
||||||
|
widget.controller.setActiveSpace(parentSpace?.id),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
parentSpace == null
|
||||||
|
? L10n.of(context)!.allSpaces
|
||||||
|
: parentSpace.getLocalizedDisplayname(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: snapshot.connectionState != ConnectionState.done
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.refresh_outlined),
|
||||||
|
onPressed: snapshot.connectionState != ConnectionState.done
|
||||||
|
? null
|
||||||
|
: _refresh,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
i--;
|
||||||
child: ListView.builder(
|
if (canLoadMore && i == spaceChildren.length) {
|
||||||
itemCount: spaceChildren.length + 1 + (canLoadMore ? 1 : 0),
|
return ListTile(
|
||||||
controller: widget.scrollController,
|
title: Text(L10n.of(context)!.loadMore),
|
||||||
itemBuilder: (context, i) {
|
trailing: const Icon(Icons.chevron_right_outlined),
|
||||||
if (i == 0) {
|
onTap: () {
|
||||||
return ListTile(
|
prevBatch = response.nextBatch;
|
||||||
leading: BackButton(
|
_refresh();
|
||||||
onPressed: () =>
|
},
|
||||||
widget.controller.setActiveSpace(parentSpace?.id),
|
);
|
||||||
),
|
}
|
||||||
title: Text(parentSpace == null
|
final spaceChild = spaceChildren[i];
|
||||||
? L10n.of(context)!.allSpaces
|
final room = client.getRoomById(spaceChild.roomId);
|
||||||
: parentSpace.getLocalizedDisplayname(
|
if (room != null && !room.isSpace) {
|
||||||
MatrixLocals(L10n.of(context)!),
|
return ChatListItem(
|
||||||
)),
|
room,
|
||||||
trailing: IconButton(
|
onLongPress: () => _onSpaceChildContextMenu(spaceChild, room),
|
||||||
icon: snapshot.connectionState != ConnectionState.done
|
activeChat: widget.controller.activeChat == room.id,
|
||||||
? const CircularProgressIndicator.adaptive()
|
);
|
||||||
: const Icon(Icons.refresh_outlined),
|
}
|
||||||
onPressed:
|
final isSpace = spaceChild.roomType == 'm.space';
|
||||||
snapshot.connectionState != ConnectionState.done
|
final topic =
|
||||||
? null
|
spaceChild.topic?.isEmpty ?? true ? null : spaceChild.topic;
|
||||||
: _refresh,
|
if (spaceChild.roomId == activeSpaceId) {
|
||||||
),
|
return SearchTitle(
|
||||||
);
|
title:
|
||||||
}
|
spaceChild.name ?? spaceChild.canonicalAlias ?? 'Space',
|
||||||
i--;
|
icon: Padding(
|
||||||
if (canLoadMore && i == spaceChildren.length) {
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
return ListTile(
|
child: Avatar(
|
||||||
title: Text(L10n.of(context)!.loadMore),
|
size: 24,
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
|
||||||
onTap: () {
|
|
||||||
prevBatch = response.nextBatch;
|
|
||||||
_refresh();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final spaceChild = spaceChildren[i];
|
|
||||||
final room = client.getRoomById(spaceChild.roomId);
|
|
||||||
if (room != null && !room.isSpace) {
|
|
||||||
return ChatListItem(
|
|
||||||
room,
|
|
||||||
onLongPress: () =>
|
|
||||||
_onSpaceChildContextMenu(spaceChild, room),
|
|
||||||
activeChat: widget.controller.activeChat == room.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final isSpace = spaceChild.roomType == 'm.space';
|
|
||||||
final topic = spaceChild.topic?.isEmpty ?? true
|
|
||||||
? null
|
|
||||||
: spaceChild.topic;
|
|
||||||
if (spaceChild.roomId == activeSpaceId) {
|
|
||||||
return SearchTitle(
|
|
||||||
title: spaceChild.name ??
|
|
||||||
spaceChild.canonicalAlias ??
|
|
||||||
'Space',
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
|
||||||
child: Avatar(
|
|
||||||
size: 24,
|
|
||||||
mxContent: spaceChild.avatarUrl,
|
|
||||||
name: spaceChild.name,
|
|
||||||
fontSize: 9,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer
|
|
||||||
.withAlpha(128),
|
|
||||||
trailing: const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Icon(Icons.edit_outlined),
|
|
||||||
),
|
|
||||||
onTap: () => _onJoinSpaceChild(spaceChild),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ListTile(
|
|
||||||
leading: Avatar(
|
|
||||||
mxContent: spaceChild.avatarUrl,
|
mxContent: spaceChild.avatarUrl,
|
||||||
name: spaceChild.name,
|
name: spaceChild.name,
|
||||||
|
fontSize: 9,
|
||||||
),
|
),
|
||||||
title: Row(
|
),
|
||||||
children: [
|
color: Theme.of(context)
|
||||||
Expanded(
|
.colorScheme
|
||||||
child: Text(
|
.secondaryContainer
|
||||||
spaceChild.name ??
|
.withAlpha(128),
|
||||||
spaceChild.canonicalAlias ??
|
trailing: const Padding(
|
||||||
L10n.of(context)!.chat,
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
maxLines: 1,
|
child: Icon(Icons.edit_outlined),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
),
|
||||||
),
|
onTap: () => _onJoinSpaceChild(spaceChild),
|
||||||
),
|
);
|
||||||
if (!isSpace) ...[
|
}
|
||||||
const Icon(
|
return ListTile(
|
||||||
Icons.people_outline,
|
leading: Avatar(
|
||||||
size: 16,
|
mxContent: spaceChild.avatarUrl,
|
||||||
),
|
name: spaceChild.name,
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
Text(
|
title: Row(
|
||||||
spaceChild.numJoinedMembers.toString(),
|
children: [
|
||||||
style: const TextStyle(fontSize: 14),
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
],
|
spaceChild.name ??
|
||||||
],
|
spaceChild.canonicalAlias ??
|
||||||
|
L10n.of(context)!.chat,
|
||||||
|
maxLines: 1,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () => _onJoinSpaceChild(spaceChild),
|
if (!isSpace) ...[
|
||||||
onLongPress: () =>
|
const Icon(
|
||||||
_onSpaceChildContextMenu(spaceChild, room),
|
Icons.people_outline,
|
||||||
subtitle: Text(
|
size: 16,
|
||||||
topic ??
|
),
|
||||||
(isSpace
|
const SizedBox(width: 4),
|
||||||
? L10n.of(context)!.enterSpace
|
Text(
|
||||||
: L10n.of(context)!.enterRoom),
|
spaceChild.numJoinedMembers.toString(),
|
||||||
maxLines: 1,
|
style: const TextStyle(fontSize: 14),
|
||||||
style: TextStyle(
|
),
|
||||||
color: Theme.of(context).colorScheme.onBackground),
|
],
|
||||||
),
|
],
|
||||||
trailing: isSpace
|
),
|
||||||
? const Icon(Icons.chevron_right_outlined)
|
onTap: () => _onJoinSpaceChild(spaceChild),
|
||||||
: null,
|
onLongPress: () => _onSpaceChildContextMenu(spaceChild, room),
|
||||||
);
|
subtitle: Text(
|
||||||
}),
|
topic ??
|
||||||
);
|
(isSpace
|
||||||
});
|
? L10n.of(context)!.enterSpace
|
||||||
|
: L10n.of(context)!.enterRoom),
|
||||||
|
maxLines: 1,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing:
|
||||||
|
isSpace ? const Icon(Icons.chevron_right_outlined) : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,12 +22,17 @@ class ChatPermissionsSettings extends StatefulWidget {
|
|||||||
|
|
||||||
class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
||||||
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
||||||
void editPowerLevel(BuildContext context, String key, int currentLevel,
|
void editPowerLevel(
|
||||||
{String? category}) async {
|
BuildContext context,
|
||||||
|
String key,
|
||||||
|
int currentLevel, {
|
||||||
|
String? category,
|
||||||
|
}) async {
|
||||||
final room = Matrix.of(context).client.getRoomById(roomId!)!;
|
final room = Matrix.of(context).client.getRoomById(roomId!)!;
|
||||||
if (!room.canSendEvent(EventTypes.RoomPowerLevels)) {
|
if (!room.canSendEvent(EventTypes.RoomPowerLevels)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.noPermission)));
|
SnackBar(content: Text(L10n.of(context)!.noPermission)),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final newLevel = await showPermissionChooser(
|
final newLevel = await showPermissionChooser(
|
||||||
@ -36,7 +41,8 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
|||||||
);
|
);
|
||||||
if (newLevel == null) return;
|
if (newLevel == null) return;
|
||||||
final content = Map<String, dynamic>.from(
|
final content = Map<String, dynamic>.from(
|
||||||
room.getState(EventTypes.RoomPowerLevels)!.content);
|
room.getState(EventTypes.RoomPowerLevels)!.content,
|
||||||
|
);
|
||||||
if (category != null) {
|
if (category != null) {
|
||||||
if (!content.containsKey(category)) {
|
if (!content.containsKey(category)) {
|
||||||
content[category] = <String, dynamic>{};
|
content[category] = <String, dynamic>{};
|
||||||
@ -74,10 +80,13 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
|||||||
title: L10n.of(context)!.replaceRoomWithNewerVersion,
|
title: L10n.of(context)!.replaceRoomWithNewerVersion,
|
||||||
actions: capabilities.mRoomVersions!.available.entries
|
actions: capabilities.mRoomVersions!.available.entries
|
||||||
.where((r) => r.key != roomVersion)
|
.where((r) => r.key != roomVersion)
|
||||||
.map((version) => AlertDialogAction(
|
.map(
|
||||||
|
(version) => AlertDialogAction(
|
||||||
key: version.key,
|
key: version.key,
|
||||||
label:
|
label:
|
||||||
'${version.key} (${version.value.toString().split('.').last})'))
|
'${version.key} (${version.value.toString().split('.').last})',
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
if (newVersion == null ||
|
if (newVersion == null ||
|
||||||
|
@ -41,7 +41,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
return Center(child: Text(L10n.of(context)!.noRoomsFound));
|
return Center(child: Text(L10n.of(context)!.noRoomsFound));
|
||||||
}
|
}
|
||||||
final powerLevelsContent = Map<String, dynamic>.from(
|
final powerLevelsContent = Map<String, dynamic>.from(
|
||||||
room.getState(EventTypes.RoomPowerLevels)!.content);
|
room.getState(EventTypes.RoomPowerLevels)!.content,
|
||||||
|
);
|
||||||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||||
..removeWhere((k, v) => v is! int);
|
..removeWhere((k, v) => v is! int);
|
||||||
final eventsPowerLevels =
|
final eventsPowerLevels =
|
||||||
@ -57,7 +58,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
permissionKey: entry.key,
|
permissionKey: entry.key,
|
||||||
permission: entry.value,
|
permission: entry.value,
|
||||||
onTap: () => controller.editPowerLevel(
|
onTap: () => controller.editPowerLevel(
|
||||||
context, entry.key, entry.value),
|
context,
|
||||||
|
entry.key,
|
||||||
|
entry.value,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(thickness: 1),
|
const Divider(thickness: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -69,21 +73,26 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Builder(builder: (context) {
|
Builder(
|
||||||
const key = 'rooms';
|
builder: (context) {
|
||||||
final int value = powerLevelsContent
|
const key = 'rooms';
|
||||||
.containsKey('notifications')
|
final int value = powerLevelsContent
|
||||||
? powerLevelsContent['notifications']['rooms'] ?? 0
|
.containsKey('notifications')
|
||||||
: 0;
|
? powerLevelsContent['notifications']['rooms'] ?? 0
|
||||||
return PermissionsListTile(
|
: 0;
|
||||||
permissionKey: key,
|
return PermissionsListTile(
|
||||||
permission: value,
|
permissionKey: key,
|
||||||
category: 'notifications',
|
permission: value,
|
||||||
onTap: () => controller.editPowerLevel(
|
category: 'notifications',
|
||||||
context, key, value,
|
onTap: () => controller.editPowerLevel(
|
||||||
category: 'notifications'),
|
context,
|
||||||
);
|
key,
|
||||||
}),
|
value,
|
||||||
|
category: 'notifications',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(thickness: 1),
|
const Divider(thickness: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -100,8 +109,11 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
category: 'events',
|
category: 'events',
|
||||||
permission: entry.value,
|
permission: entry.value,
|
||||||
onTap: () => controller.editPowerLevel(
|
onTap: () => controller.editPowerLevel(
|
||||||
context, entry.key, entry.value,
|
context,
|
||||||
category: 'events'),
|
entry.key,
|
||||||
|
entry.value,
|
||||||
|
category: 'events',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (room.canSendEvent(EventTypes.RoomTombstone)) ...{
|
if (room.canSendEvent(EventTypes.RoomTombstone)) ...{
|
||||||
const Divider(thickness: 1),
|
const Divider(thickness: 1),
|
||||||
@ -110,8 +122,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2));
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final String roomVersion = room
|
final String roomVersion = room
|
||||||
.getState(EventTypes.RoomCreate)!
|
.getState(EventTypes.RoomCreate)!
|
||||||
@ -120,7 +134,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
'${L10n.of(context)!.roomVersion}: $roomVersion'),
|
'${L10n.of(context)!.roomVersion}: $roomVersion',
|
||||||
|
),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
controller.updateRoomAction(snapshot.data!),
|
controller.updateRoomAction(snapshot.data!),
|
||||||
);
|
);
|
||||||
|
@ -118,8 +118,9 @@ class ConnectPageController extends State<ConnectPage> {
|
|||||||
List<IdentityProvider>? get identityProviders {
|
List<IdentityProvider>? get identityProviders {
|
||||||
final loginTypes = _rawLoginTypes;
|
final loginTypes = _rawLoginTypes;
|
||||||
if (loginTypes == null) return null;
|
if (loginTypes == null) return null;
|
||||||
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere((flow) =>
|
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
|
||||||
flow['type'] == AuthenticationTypes.sso)['identity_providers'];
|
(flow) => flow['type'] == AuthenticationTypes.sso,
|
||||||
|
)['identity_providers'];
|
||||||
final list = (rawProviders as List)
|
final list = (rawProviders as List)
|
||||||
.map((json) => IdentityProvider.fromJson(json))
|
.map((json) => IdentityProvider.fromJson(json))
|
||||||
.toList();
|
.toList();
|
||||||
@ -163,9 +164,11 @@ class ConnectPageController extends State<ConnectPage> {
|
|||||||
RequestType.GET,
|
RequestType.GET,
|
||||||
'/client/r0/login',
|
'/client/r0/login',
|
||||||
)
|
)
|
||||||
.then((loginTypes) => setState(() {
|
.then(
|
||||||
_rawLoginTypes = loginTypes;
|
(loginTypes) => setState(() {
|
||||||
}));
|
_rawLoginTypes = loginTypes;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,17 +174,20 @@ class ConnectPageView extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Image.network(
|
: Image.network(
|
||||||
Uri.parse(identityProviders.single.icon!)
|
Uri.parse(identityProviders.single.icon!)
|
||||||
.getDownloadLink(Matrix.of(context)
|
.getDownloadLink(
|
||||||
.getLoginClient())
|
Matrix.of(context).getLoginClient(),
|
||||||
|
)
|
||||||
.toString(),
|
.toString(),
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
onPressed: () => controller
|
onPressed: () => controller
|
||||||
.ssoLoginAction(identityProviders.single.id!),
|
.ssoLoginAction(identityProviders.single.id!),
|
||||||
label: Text(identityProviders.single.name ??
|
label: Text(
|
||||||
identityProviders.single.brand ??
|
identityProviders.single.name ??
|
||||||
L10n.of(context)!.loginWithOneClick),
|
identityProviders.single.brand ??
|
||||||
|
L10n.of(context)!.loginWithOneClick,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Wrap(
|
: Wrap(
|
||||||
|
@ -37,7 +37,8 @@ class SsoButton extends StatelessWidget {
|
|||||||
: Image.network(
|
: Image.network(
|
||||||
Uri.parse(identityProvider.icon!)
|
Uri.parse(identityProvider.icon!)
|
||||||
.getDownloadLink(
|
.getDownloadLink(
|
||||||
Matrix.of(context).getLoginClient())
|
Matrix.of(context).getLoginClient(),
|
||||||
|
)
|
||||||
.toString(),
|
.toString(),
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
|
@ -35,7 +35,8 @@ class DevicesSettingsView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
if (!snapshot.hasData || controller.devices == null) {
|
if (!snapshot.hasData || controller.devices == null) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: controller.notThisDevice.length + 1,
|
itemCount: controller.notThisDevice.length + 1,
|
||||||
@ -63,12 +64,14 @@ class DevicesSettingsView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
trailing: controller.loadingDeletingDevices
|
trailing: controller.loadingDeletingDevices
|
||||||
? const CircularProgressIndicator.adaptive(
|
? const CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2)
|
strokeWidth: 2,
|
||||||
|
)
|
||||||
: const Icon(Icons.delete_outline),
|
: const Icon(Icons.delete_outline),
|
||||||
onTap: controller.loadingDeletingDevices
|
onTap: controller.loadingDeletingDevices
|
||||||
? null
|
? null
|
||||||
: () => controller.removeDevicesAction(
|
: () => controller.removeDevicesAction(
|
||||||
controller.notThisDevice),
|
controller.notThisDevice,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Center(
|
Center(
|
||||||
|
@ -135,8 +135,9 @@ class UserDeviceListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
L10n.of(context)!.lastActiveAgo(
|
L10n.of(context)!.lastActiveAgo(
|
||||||
DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0)
|
DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0)
|
||||||
.localizedTimeShort(context)),
|
.localizedTimeShort(context),
|
||||||
|
),
|
||||||
style: const TextStyle(fontWeight: FontWeight.w300),
|
style: const TextStyle(fontWeight: FontWeight.w300),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -36,9 +36,12 @@ import 'package:fluffychat/widgets/avatar.dart';
|
|||||||
import 'pip/pip_view.dart';
|
import 'pip/pip_view.dart';
|
||||||
|
|
||||||
class _StreamView extends StatelessWidget {
|
class _StreamView extends StatelessWidget {
|
||||||
const _StreamView(this.wrappedStream,
|
const _StreamView(
|
||||||
{Key? key, this.mainView = false, required this.matrixClient})
|
this.wrappedStream, {
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
this.mainView = false,
|
||||||
|
required this.matrixClient,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final WrappedMediaStream wrappedStream;
|
final WrappedMediaStream wrappedStream;
|
||||||
final Client matrixClient;
|
final Client matrixClient;
|
||||||
@ -67,43 +70,48 @@ class _StreamView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (videoMuted)
|
if (videoMuted)
|
||||||
Container(
|
Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
),
|
),
|
||||||
if (!videoMuted)
|
if (!videoMuted)
|
||||||
RTCVideoView(
|
RTCVideoView(
|
||||||
// yes, it must explicitly be casted even though I do not feel
|
// yes, it must explicitly be casted even though I do not feel
|
||||||
// comfortable with it...
|
// comfortable with it...
|
||||||
wrappedStream.renderer as RTCVideoRenderer,
|
wrappedStream.renderer as RTCVideoRenderer,
|
||||||
mirror: mirrored,
|
mirror: mirrored,
|
||||||
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||||
),
|
),
|
||||||
if (videoMuted)
|
if (videoMuted)
|
||||||
Positioned(
|
Positioned(
|
||||||
child: Avatar(
|
child: Avatar(
|
||||||
mxContent: avatarUrl,
|
mxContent: avatarUrl,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
size: mainView ? 96 : 48,
|
size: mainView ? 96 : 48,
|
||||||
client: matrixClient,
|
client: matrixClient,
|
||||||
// textSize: mainView ? 36 : 24,
|
// textSize: mainView ? 36 : 24,
|
||||||
// matrixClient: matrixClient,
|
// matrixClient: matrixClient,
|
||||||
)),
|
),
|
||||||
if (!isScreenSharing)
|
),
|
||||||
Positioned(
|
if (!isScreenSharing)
|
||||||
left: 4.0,
|
Positioned(
|
||||||
bottom: 4.0,
|
left: 4.0,
|
||||||
child: Icon(audioMuted ? Icons.mic_off : Icons.mic,
|
bottom: 4.0,
|
||||||
color: Colors.white, size: 18.0),
|
child: Icon(
|
||||||
)
|
audioMuted ? Icons.mic_off : Icons.mic,
|
||||||
],
|
color: Colors.white,
|
||||||
));
|
size: 18.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,14 +122,14 @@ class Calling extends StatefulWidget {
|
|||||||
final CallSession call;
|
final CallSession call;
|
||||||
final Client client;
|
final Client client;
|
||||||
|
|
||||||
const Calling(
|
const Calling({
|
||||||
{required this.context,
|
required this.context,
|
||||||
required this.call,
|
required this.call,
|
||||||
required this.client,
|
required this.client,
|
||||||
required this.callId,
|
required this.callId,
|
||||||
this.onClear,
|
this.onClear,
|
||||||
Key? key})
|
Key? key,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MyCallingPage createState() => MyCallingPage();
|
MyCallingPage createState() => MyCallingPage();
|
||||||
@ -206,7 +214,8 @@ class MyCallingPage extends State<Calling> {
|
|||||||
event == CallEvent.kRemoteHoldUnhold) {
|
event == CallEvent.kRemoteHoldUnhold) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
Logs().i(
|
Logs().i(
|
||||||
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}');
|
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_state = call.state;
|
_state = call.state;
|
||||||
@ -239,7 +248,9 @@ class MyCallingPage extends State<Calling> {
|
|||||||
|
|
||||||
void _resizeLocalVideo(Orientation orientation) {
|
void _resizeLocalVideo(Orientation orientation) {
|
||||||
final shortSide = min(
|
final shortSide = min(
|
||||||
MediaQuery.of(context).size.width, MediaQuery.of(context).size.height);
|
MediaQuery.of(context).size.width,
|
||||||
|
MediaQuery.of(context).size.height,
|
||||||
|
);
|
||||||
_localVideoMargin = remoteStream != null
|
_localVideoMargin = remoteStream != null
|
||||||
? const EdgeInsets.only(top: 20.0, right: 20.0)
|
? const EdgeInsets.only(top: 20.0, right: 20.0)
|
||||||
: EdgeInsets.zero;
|
: EdgeInsets.zero;
|
||||||
@ -304,8 +315,9 @@ class MyCallingPage extends State<Calling> {
|
|||||||
foregroundTaskOptions: const ForegroundTaskOptions(),
|
foregroundTaskOptions: const ForegroundTaskOptions(),
|
||||||
);
|
);
|
||||||
FlutterForegroundTask.startService(
|
FlutterForegroundTask.startService(
|
||||||
notificationTitle: L10n.of(context)!.screenSharingTitle,
|
notificationTitle: L10n.of(context)!.screenSharingTitle,
|
||||||
notificationText: L10n.of(context)!.screenSharingDetail);
|
notificationText: L10n.of(context)!.screenSharingDetail,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
FlutterForegroundTask.stopService();
|
FlutterForegroundTask.stopService();
|
||||||
}
|
}
|
||||||
@ -331,7 +343,8 @@ class MyCallingPage extends State<Calling> {
|
|||||||
void _switchCamera() async {
|
void _switchCamera() async {
|
||||||
if (call.localUserMediaStream != null) {
|
if (call.localUserMediaStream != null) {
|
||||||
await Helper.switchCamera(
|
await Helper.switchCamera(
|
||||||
call.localUserMediaStream!.stream!.getVideoTracks()[0]);
|
call.localUserMediaStream!.stream!.getVideoTracks()[0],
|
||||||
|
);
|
||||||
if (PlatformInfos.isMobile) {
|
if (PlatformInfos.isMobile) {
|
||||||
call.facingMode == 'user'
|
call.facingMode == 'user'
|
||||||
? call.facingMode = 'environment'
|
? call.facingMode = 'environment'
|
||||||
@ -473,22 +486,27 @@ class MyCallingPage extends State<Calling> {
|
|||||||
} else if (call.remoteOnHold) {
|
} else if (call.remoteOnHold) {
|
||||||
title = 'You held the call.';
|
title = 'You held the call.';
|
||||||
}
|
}
|
||||||
stackWidgets.add(Center(
|
stackWidgets.add(
|
||||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
Center(
|
||||||
const Icon(
|
child: Column(
|
||||||
Icons.pause,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
size: 48.0,
|
children: [
|
||||||
color: Colors.white,
|
const Icon(
|
||||||
|
Icons.pause,
|
||||||
|
size: 48.0,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
title,
|
);
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 24.0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
));
|
|
||||||
return stackWidgets;
|
return stackWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,10 +520,15 @@ class MyCallingPage extends State<Calling> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primaryStream != null) {
|
if (primaryStream != null) {
|
||||||
stackWidgets.add(Center(
|
stackWidgets.add(
|
||||||
child: _StreamView(primaryStream,
|
Center(
|
||||||
mainView: true, matrixClient: widget.client),
|
child: _StreamView(
|
||||||
));
|
primaryStream,
|
||||||
|
mainView: true,
|
||||||
|
matrixClient: widget.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFloating || !connected) {
|
if (isFloating || !connected) {
|
||||||
@ -522,47 +545,58 @@ class MyCallingPage extends State<Calling> {
|
|||||||
|
|
||||||
if (call.remoteScreenSharingStream != null) {
|
if (call.remoteScreenSharingStream != null) {
|
||||||
final remoteUserMediaStream = call.remoteUserMediaStream;
|
final remoteUserMediaStream = call.remoteUserMediaStream;
|
||||||
secondaryStreamViews.add(SizedBox(
|
secondaryStreamViews.add(
|
||||||
width: _localVideoWidth,
|
SizedBox(
|
||||||
height: _localVideoHeight,
|
width: _localVideoWidth,
|
||||||
child: _StreamView(remoteUserMediaStream!, matrixClient: widget.client),
|
height: _localVideoHeight,
|
||||||
));
|
child:
|
||||||
|
_StreamView(remoteUserMediaStream!, matrixClient: widget.client),
|
||||||
|
),
|
||||||
|
);
|
||||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
final localStream =
|
final localStream =
|
||||||
call.localUserMediaStream ?? call.localScreenSharingStream;
|
call.localUserMediaStream ?? call.localScreenSharingStream;
|
||||||
if (localStream != null && !isFloating) {
|
if (localStream != null && !isFloating) {
|
||||||
secondaryStreamViews.add(SizedBox(
|
secondaryStreamViews.add(
|
||||||
width: _localVideoWidth,
|
SizedBox(
|
||||||
height: _localVideoHeight,
|
width: _localVideoWidth,
|
||||||
child: _StreamView(localStream, matrixClient: widget.client),
|
height: _localVideoHeight,
|
||||||
));
|
child: _StreamView(localStream, matrixClient: widget.client),
|
||||||
|
),
|
||||||
|
);
|
||||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call.localScreenSharingStream != null && !isFloating) {
|
if (call.localScreenSharingStream != null && !isFloating) {
|
||||||
secondaryStreamViews.add(SizedBox(
|
secondaryStreamViews.add(
|
||||||
width: _localVideoWidth,
|
SizedBox(
|
||||||
height: _localVideoHeight,
|
width: _localVideoWidth,
|
||||||
child: _StreamView(call.remoteUserMediaStream!,
|
height: _localVideoHeight,
|
||||||
matrixClient: widget.client),
|
child: _StreamView(
|
||||||
));
|
call.remoteUserMediaStream!,
|
||||||
|
matrixClient: widget.client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondaryStreamViews.isNotEmpty) {
|
if (secondaryStreamViews.isNotEmpty) {
|
||||||
stackWidgets.add(Container(
|
stackWidgets.add(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
|
Container(
|
||||||
alignment: Alignment.bottomRight,
|
padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
|
||||||
child: Container(
|
alignment: Alignment.bottomRight,
|
||||||
width: _localVideoWidth,
|
child: Container(
|
||||||
margin: _localVideoMargin,
|
width: _localVideoWidth,
|
||||||
child: Column(
|
margin: _localVideoMargin,
|
||||||
children: secondaryStreamViews,
|
child: Column(
|
||||||
|
children: secondaryStreamViews,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stackWidgets;
|
return stackWidgets;
|
||||||
@ -570,27 +604,31 @@ class MyCallingPage extends State<Calling> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PIPView(builder: (context, isFloating) {
|
return PIPView(
|
||||||
return Scaffold(
|
builder: (context, isFloating) {
|
||||||
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: !isFloating,
|
resizeToAvoidBottomInset: !isFloating,
|
||||||
floatingActionButtonLocation:
|
floatingActionButtonLocation:
|
||||||
FloatingActionButtonLocation.centerFloat,
|
FloatingActionButtonLocation.centerFloat,
|
||||||
floatingActionButton: SizedBox(
|
floatingActionButton: SizedBox(
|
||||||
width: 320.0,
|
width: 320.0,
|
||||||
height: 150.0,
|
height: 150.0,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: _buildActionButtons(isFloating))),
|
children: _buildActionButtons(isFloating),
|
||||||
|
),
|
||||||
|
),
|
||||||
body: OrientationBuilder(
|
body: OrientationBuilder(
|
||||||
builder: (BuildContext context, Orientation orientation) {
|
builder: (BuildContext context, Orientation orientation) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
child: Stack(children: [
|
child: Stack(
|
||||||
..._buildContent(orientation, isFloating),
|
children: [
|
||||||
if (!isFloating)
|
..._buildContent(orientation, isFloating),
|
||||||
Positioned(
|
if (!isFloating)
|
||||||
|
Positioned(
|
||||||
top: 24.0,
|
top: 24.0,
|
||||||
left: 24.0,
|
left: 24.0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@ -599,9 +637,15 @@ class MyCallingPage extends State<Calling> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
PIPView.of(context)?.setFloating(true);
|
PIPView.of(context)?.setFloating(true);
|
||||||
},
|
},
|
||||||
))
|
),
|
||||||
]));
|
)
|
||||||
}));
|
],
|
||||||
});
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,9 +198,11 @@ class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
|
|||||||
: Tween<Offset>(
|
: Tween<Offset>(
|
||||||
begin: _dragOffset,
|
begin: _dragOffset,
|
||||||
end: calculatedOffset,
|
end: calculatedOffset,
|
||||||
).transform(_dragAnimationController.isAnimating
|
).transform(
|
||||||
? dragAnimationValue
|
_dragAnimationController.isAnimating
|
||||||
: toggleFloatingAnimationValue);
|
? dragAnimationValue
|
||||||
|
: toggleFloatingAnimationValue,
|
||||||
|
);
|
||||||
final borderRadius = Tween<double>(
|
final borderRadius = Tween<double>(
|
||||||
begin: 0,
|
begin: 0,
|
||||||
end: 10,
|
end: 10,
|
||||||
|
@ -21,52 +21,54 @@ class HomeserverBottomSheet extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(homeserver.homeserver.baseUrl.host),
|
title: Text(homeserver.homeserver.baseUrl.host),
|
||||||
),
|
),
|
||||||
body: ListView(children: [
|
body: ListView(
|
||||||
if (description != null && description.isNotEmpty)
|
children: [
|
||||||
|
if (description != null && description.isNotEmpty)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.info_outlined),
|
||||||
|
title: Text(description),
|
||||||
|
),
|
||||||
|
if (jurisdiction != null && jurisdiction.isNotEmpty)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.location_city_outlined),
|
||||||
|
title: Text(jurisdiction),
|
||||||
|
),
|
||||||
|
if (homeserverSoftware != null && homeserverSoftware.isNotEmpty)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.domain_outlined),
|
||||||
|
title: Text(homeserverSoftware),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outlined),
|
onTap: () =>
|
||||||
title: Text(description),
|
launchUrlString(homeserver.homeserver.baseUrl.toString()),
|
||||||
|
leading: const Icon(Icons.link_outlined),
|
||||||
|
title: Text(homeserver.homeserver.baseUrl.toString()),
|
||||||
),
|
),
|
||||||
if (jurisdiction != null && jurisdiction.isNotEmpty)
|
if (registration != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.location_city_outlined),
|
onTap: () => launchUrlString(registration.toString()),
|
||||||
title: Text(jurisdiction),
|
leading: const Icon(Icons.person_add_outlined),
|
||||||
),
|
title: Text(registration.toString()),
|
||||||
if (homeserverSoftware != null && homeserverSoftware.isNotEmpty)
|
),
|
||||||
ListTile(
|
if (rules != null)
|
||||||
leading: const Icon(Icons.domain_outlined),
|
ListTile(
|
||||||
title: Text(homeserverSoftware),
|
onTap: () => launchUrlString(rules.toString()),
|
||||||
),
|
leading: const Icon(Icons.visibility_outlined),
|
||||||
ListTile(
|
title: Text(rules.toString()),
|
||||||
onTap: () =>
|
),
|
||||||
launchUrlString(homeserver.homeserver.baseUrl.toString()),
|
if (privacy != null)
|
||||||
leading: const Icon(Icons.link_outlined),
|
ListTile(
|
||||||
title: Text(homeserver.homeserver.baseUrl.toString()),
|
onTap: () => launchUrlString(privacy.toString()),
|
||||||
),
|
leading: const Icon(Icons.shield_outlined),
|
||||||
if (registration != null)
|
title: Text(privacy.toString()),
|
||||||
ListTile(
|
),
|
||||||
onTap: () => launchUrlString(registration.toString()),
|
if (responseTime != null)
|
||||||
leading: const Icon(Icons.person_add_outlined),
|
ListTile(
|
||||||
title: Text(registration.toString()),
|
leading: const Icon(Icons.timer_outlined),
|
||||||
),
|
title: Text('${responseTime.inMilliseconds}ms'),
|
||||||
if (rules != null)
|
),
|
||||||
ListTile(
|
],
|
||||||
onTap: () => launchUrlString(rules.toString()),
|
),
|
||||||
leading: const Icon(Icons.visibility_outlined),
|
|
||||||
title: Text(rules.toString()),
|
|
||||||
),
|
|
||||||
if (privacy != null)
|
|
||||||
ListTile(
|
|
||||||
onTap: () => launchUrlString(privacy.toString()),
|
|
||||||
leading: const Icon(Icons.shield_outlined),
|
|
||||||
title: Text(privacy.toString()),
|
|
||||||
),
|
|
||||||
if (responseTime != null)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.timer_outlined),
|
|
||||||
title: Text('${responseTime.inMilliseconds}ms'),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,10 +51,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
Hive.openBox('test').then((value) => null).catchError(
|
Hive.openBox('test').then((value) => null).catchError(
|
||||||
(e, s) async {
|
(e, s) async {
|
||||||
await showOkAlertDialog(
|
await showOkAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.indexedDbErrorTitle,
|
title: L10n.of(context)!.indexedDbErrorTitle,
|
||||||
message: L10n.of(context)!.indexedDbErrorLong,
|
message: L10n.of(context)!.indexedDbErrorLong,
|
||||||
onWillPop: () async => false);
|
onWillPop: () async => false,
|
||||||
|
);
|
||||||
_checkTorBrowser();
|
_checkTorBrowser();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -85,9 +86,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
|
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
|
||||||
.where((element) =>
|
.where(
|
||||||
element.homeserver.baseUrl.host.contains(searchTerm) ||
|
(element) =>
|
||||||
(element.homeserver.description?.contains(searchTerm) ?? false))
|
element.homeserver.baseUrl.host.contains(searchTerm) ||
|
||||||
|
(element.homeserver.description?.contains(searchTerm) ?? false),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
void _loadHomeserverList() async {
|
void _loadHomeserverList() async {
|
||||||
@ -186,15 +189,16 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||||||
final file = await FilePickerCross.importFromStorage();
|
final file = await FilePickerCross.importFromStorage();
|
||||||
if (file.fileName == null) return;
|
if (file.fileName == null) return;
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
try {
|
try {
|
||||||
final client = Matrix.of(context).getLoginClient();
|
final client = Matrix.of(context).getLoginClient();
|
||||||
await client.importDump(file.toString());
|
await client.importDump(file.toString());
|
||||||
Matrix.of(context).initMatrix();
|
Matrix.of(context).initMatrix();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('Future error:', e, s);
|
Logs().e('Future error:', e, s);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +65,11 @@ class HomeserverPickerView extends StatelessWidget {
|
|||||||
child: benchmarkResults == null
|
child: benchmarkResults == null
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(12.0),
|
padding: EdgeInsets.all(12.0),
|
||||||
child:
|
child: CircularProgressIndicator
|
||||||
CircularProgressIndicator.adaptive(),
|
.adaptive(),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: controller.filteredHomeservers
|
children: controller.filteredHomeservers
|
||||||
.map(
|
.map(
|
||||||
@ -82,19 +83,20 @@ class HomeserverPickerView extends StatelessWidget {
|
|||||||
.showServerInfo(server),
|
.showServerInfo(server),
|
||||||
),
|
),
|
||||||
onTap: () => controller.setServer(
|
onTap: () => controller.setServer(
|
||||||
server
|
server.homeserver.baseUrl.host,
|
||||||
.homeserver.baseUrl.host),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
server.homeserver.baseUrl.host,
|
server.homeserver.baseUrl.host,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.black),
|
color: Colors.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
server.homeserver.description ??
|
server.homeserver.description ??
|
||||||
'',
|
'',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color:
|
color: Colors.grey.shade700,
|
||||||
Colors.grey.shade700),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -42,12 +42,13 @@ class ImageViewerView extends StatelessWidget {
|
|||||||
if (PlatformInfos.isMobile)
|
if (PlatformInfos.isMobile)
|
||||||
// Use builder context to correctly position the share dialog on iPad
|
// Use builder context to correctly position the share dialog on iPad
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) => IconButton(
|
builder: (context) => IconButton(
|
||||||
onPressed: () => controller.shareFileAction(context),
|
onPressed: () => controller.shareFileAction(context),
|
||||||
tooltip: L10n.of(context)!.share,
|
tooltip: L10n.of(context)!.share,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.adaptive.share_outlined),
|
icon: Icon(Icons.adaptive.share_outlined),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: InteractiveViewer(
|
body: InteractiveViewer(
|
||||||
|
@ -71,8 +71,11 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
|||||||
future: () => room.invite(id),
|
future: () => room.invite(id),
|
||||||
);
|
);
|
||||||
if (success.error == null) {
|
if (success.error == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup)));
|
SnackBar(
|
||||||
|
content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +102,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
|||||||
response = await matrix.client.searchUserDirectory(text, limit: 10);
|
response = await matrix.client.searchUserDirectory(text, limit: 10);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text((e).toLocalizedString(context))));
|
SnackBar(content: Text((e).toLocalizedString(context))),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => loading = false);
|
setState(() => loading = false);
|
||||||
@ -108,19 +112,25 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
|||||||
foundProfiles = List<Profile>.from(response.results);
|
foundProfiles = List<Profile>.from(response.results);
|
||||||
if (text.isValidMatrixId &&
|
if (text.isValidMatrixId &&
|
||||||
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
|
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
|
||||||
setState(() => foundProfiles = [
|
setState(
|
||||||
Profile.fromJson({'user_id': text}),
|
() => foundProfiles = [
|
||||||
]);
|
Profile.fromJson({'user_id': text}),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final participants = Matrix.of(context)
|
final participants = Matrix.of(context)
|
||||||
.client
|
.client
|
||||||
.getRoomById(roomId!)!
|
.getRoomById(roomId!)!
|
||||||
.getParticipants()
|
.getParticipants()
|
||||||
.where((user) =>
|
.where(
|
||||||
[Membership.join, Membership.invite].contains(user.membership))
|
(user) =>
|
||||||
|
[Membership.join, Membership.invite].contains(user.membership),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
foundProfiles.removeWhere((profile) =>
|
foundProfiles.removeWhere(
|
||||||
participants.indexWhere((u) => u.id == profile.userId) != -1);
|
(profile) =>
|
||||||
|
participants.indexWhere((u) => u.id == profile.userId) != -1,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,9 @@ class InvitationSelectionView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subtitle: Text(controller.foundProfiles[i].userId),
|
subtitle: Text(controller.foundProfiles[i].userId),
|
||||||
onTap: () => controller.inviteAction(
|
onTap: () => controller.inviteAction(
|
||||||
context, controller.foundProfiles[i].userId),
|
context,
|
||||||
|
controller.foundProfiles[i].userId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: FutureBuilder<List<User>>(
|
: FutureBuilder<List<User>>(
|
||||||
@ -106,7 +108,8 @@ class InvitationSelectionView extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.secondary),
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
controller.inviteAction(context, contacts[i].id),
|
controller.inviteAction(context, contacts[i].id),
|
||||||
|
@ -70,19 +70,20 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
if (input.isEmpty) return;
|
if (input.isEmpty) return;
|
||||||
|
|
||||||
final valid = await showFutureLoadingDialog(
|
final valid = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
// make sure the loading spinner shows before we test the keys
|
// make sure the loading spinner shows before we test the keys
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
var valid = false;
|
var valid = false;
|
||||||
try {
|
try {
|
||||||
await widget.request.openSSSS(keyOrPassphrase: input);
|
await widget.request.openSSSS(keyOrPassphrase: input);
|
||||||
valid = true;
|
valid = true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
return valid;
|
return valid;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (valid.error != null) {
|
if (valid.error != null) {
|
||||||
await showOkAlertDialog(
|
await showOkAlertDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
@ -117,8 +118,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(L10n.of(context)!.askSSSSSign,
|
Text(
|
||||||
style: const TextStyle(fontSize: 20)),
|
L10n.of(context)!.askSSSSSign,
|
||||||
|
style: const TextStyle(fontSize: 20),
|
||||||
|
),
|
||||||
Container(height: 10),
|
Container(height: 10),
|
||||||
TextField(
|
TextField(
|
||||||
controller: textEditingController,
|
controller: textEditingController,
|
||||||
@ -141,18 +144,22 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
buttons.add(TextButton(
|
buttons.add(
|
||||||
child: Text(
|
TextButton(
|
||||||
L10n.of(context)!.submit,
|
child: Text(
|
||||||
|
L10n.of(context)!.submit,
|
||||||
|
),
|
||||||
|
onPressed: () => checkInput(textEditingController.text),
|
||||||
),
|
),
|
||||||
onPressed: () => checkInput(textEditingController.text),
|
);
|
||||||
));
|
buttons.add(
|
||||||
buttons.add(TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
L10n.of(context)!.skip,
|
L10n.of(context)!.skip,
|
||||||
|
),
|
||||||
|
onPressed: () => widget.request.openSSSS(skip: true),
|
||||||
),
|
),
|
||||||
onPressed: () => widget.request.openSSSS(skip: true),
|
);
|
||||||
));
|
|
||||||
break;
|
break;
|
||||||
case KeyVerificationState.askAccept:
|
case KeyVerificationState.askAccept:
|
||||||
title = Text(L10n.of(context)!.newVerificationRequest);
|
title = Text(L10n.of(context)!.newVerificationRequest);
|
||||||
@ -171,19 +178,23 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
buttons.add(TextButton.icon(
|
buttons.add(
|
||||||
icon: const Icon(Icons.close),
|
TextButton.icon(
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
icon: const Icon(Icons.close),
|
||||||
label: Text(L10n.of(context)!.reject),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
onPressed: () => widget.request
|
label: Text(L10n.of(context)!.reject),
|
||||||
.rejectVerification()
|
onPressed: () => widget.request
|
||||||
.then((_) => Navigator.of(context, rootNavigator: false).pop()),
|
.rejectVerification()
|
||||||
));
|
.then((_) => Navigator.of(context, rootNavigator: false).pop()),
|
||||||
buttons.add(TextButton.icon(
|
),
|
||||||
icon: const Icon(Icons.check),
|
);
|
||||||
label: Text(L10n.of(context)!.accept),
|
buttons.add(
|
||||||
onPressed: () => widget.request.acceptVerification(),
|
TextButton.icon(
|
||||||
));
|
icon: const Icon(Icons.check),
|
||||||
|
label: Text(L10n.of(context)!.accept),
|
||||||
|
onPressed: () => widget.request.acceptVerification(),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case KeyVerificationState.waitingAccept:
|
case KeyVerificationState.waitingAccept:
|
||||||
body = Center(
|
body = Center(
|
||||||
@ -245,19 +256,23 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
buttons.add(TextButton.icon(
|
buttons.add(
|
||||||
icon: const Icon(Icons.close),
|
TextButton.icon(
|
||||||
style: TextButton.styleFrom(
|
icon: const Icon(Icons.close),
|
||||||
foregroundColor: Colors.red,
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
label: Text(L10n.of(context)!.theyDontMatch),
|
||||||
|
onPressed: () => widget.request.rejectSas(),
|
||||||
),
|
),
|
||||||
label: Text(L10n.of(context)!.theyDontMatch),
|
);
|
||||||
onPressed: () => widget.request.rejectSas(),
|
buttons.add(
|
||||||
));
|
TextButton.icon(
|
||||||
buttons.add(TextButton.icon(
|
icon: const Icon(Icons.check_outlined),
|
||||||
icon: const Icon(Icons.check_outlined),
|
label: Text(L10n.of(context)!.theyMatch),
|
||||||
label: Text(L10n.of(context)!.theyMatch),
|
onPressed: () => widget.request.acceptSas(),
|
||||||
onPressed: () => widget.request.acceptSas(),
|
),
|
||||||
));
|
);
|
||||||
break;
|
break;
|
||||||
case KeyVerificationState.waitingSas:
|
case KeyVerificationState.waitingSas:
|
||||||
final acceptText = widget.request.sasTypes.contains('emoji')
|
final acceptText = widget.request.sasTypes.contains('emoji')
|
||||||
@ -279,8 +294,11 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
body = Column(
|
body = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(Icons.check_circle_outlined,
|
const Icon(
|
||||||
color: Colors.green, size: 128.0),
|
Icons.check_circle_outlined,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 128.0,
|
||||||
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
L10n.of(context)!.verifySuccess,
|
L10n.of(context)!.verifySuccess,
|
||||||
@ -288,12 +306,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
buttons.add(TextButton(
|
buttons.add(
|
||||||
child: Text(
|
TextButton(
|
||||||
L10n.of(context)!.close,
|
child: Text(
|
||||||
|
L10n.of(context)!.close,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
);
|
||||||
));
|
|
||||||
break;
|
break;
|
||||||
case KeyVerificationState.error:
|
case KeyVerificationState.error:
|
||||||
body = Column(
|
body = Column(
|
||||||
@ -307,12 +327,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
buttons.add(TextButton(
|
buttons.add(
|
||||||
child: Text(
|
TextButton(
|
||||||
L10n.of(context)!.close,
|
child: Text(
|
||||||
|
L10n.of(context)!.close,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
);
|
||||||
));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -350,7 +372,8 @@ class _Emoji extends StatelessWidget {
|
|||||||
return emoji.name;
|
return emoji.name;
|
||||||
}
|
}
|
||||||
final translations = Map<String, String?>.from(
|
final translations = Map<String, String?>.from(
|
||||||
sasEmoji[emoji.number]['translated_descriptions']);
|
sasEmoji[emoji.number]['translated_descriptions'],
|
||||||
|
);
|
||||||
translations['en'] = emoji.name;
|
translations['en'] = emoji.name;
|
||||||
for (final locale in window.locales) {
|
for (final locale in window.locales) {
|
||||||
final wantLocaleParts = locale.toString().split('_');
|
final wantLocaleParts = locale.toString().split('_');
|
||||||
|
@ -67,15 +67,17 @@ class LoginController extends State<Login> {
|
|||||||
} else {
|
} else {
|
||||||
identifier = AuthenticationUserIdentifier(user: username);
|
identifier = AuthenticationUserIdentifier(user: username);
|
||||||
}
|
}
|
||||||
await matrix.getLoginClient().login(LoginType.mLoginPassword,
|
await matrix.getLoginClient().login(
|
||||||
identifier: identifier,
|
LoginType.mLoginPassword,
|
||||||
// To stay compatible with older server versions
|
identifier: identifier,
|
||||||
// ignore: deprecated_member_use
|
// To stay compatible with older server versions
|
||||||
user: identifier.type == AuthenticationIdentifierTypes.userId
|
// ignore: deprecated_member_use
|
||||||
? username
|
user: identifier.type == AuthenticationIdentifierTypes.userId
|
||||||
: null,
|
? username
|
||||||
password: passwordController.text,
|
: null,
|
||||||
initialDeviceDisplayName: PlatformInfos.clientName);
|
password: passwordController.text,
|
||||||
|
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||||
|
);
|
||||||
} on MatrixException catch (exception) {
|
} on MatrixException catch (exception) {
|
||||||
setState(() => passwordError = exception.errorMessage);
|
setState(() => passwordError = exception.errorMessage);
|
||||||
return setState(() => loading = false);
|
return setState(() => loading = false);
|
||||||
@ -121,7 +123,8 @@ class LoginController extends State<Login> {
|
|||||||
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
||||||
// okay, the server we checked does not appear to be a matrix server
|
// okay, the server we checked does not appear to be a matrix server
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'$newDomain is not running a homeserver, asking to use $oldHomeserver');
|
'$newDomain is not running a homeserver, asking to use $oldHomeserver',
|
||||||
|
);
|
||||||
final dialogResult = await showOkCancelAlertDialog(
|
final dialogResult = await showOkCancelAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
@ -230,7 +233,8 @@ class LoginController extends State<Login> {
|
|||||||
);
|
);
|
||||||
if (success.error == null) {
|
if (success.error == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)));
|
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)),
|
||||||
|
);
|
||||||
usernameController.text = input.single;
|
usernameController.text = input.single;
|
||||||
passwordController.text = password.single;
|
passwordController.text = password.single;
|
||||||
login();
|
login();
|
||||||
|
@ -19,125 +19,131 @@ class LoginView extends StatelessWidget {
|
|||||||
automaticallyImplyLeading: !controller.loading,
|
automaticallyImplyLeading: !controller.loading,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text(
|
title: Text(
|
||||||
L10n.of(context)!.logInTo(Matrix.of(context)
|
L10n.of(context)!.logInTo(
|
||||||
.getLoginClient()
|
Matrix.of(context)
|
||||||
.homeserver
|
.getLoginClient()
|
||||||
.toString()
|
.homeserver
|
||||||
.replaceFirst('https://', '')),
|
.toString()
|
||||||
|
.replaceFirst('https://', ''),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Builder(builder: (context) {
|
body: Builder(
|
||||||
return AutofillGroup(
|
builder: (context) {
|
||||||
child: ListView(
|
return AutofillGroup(
|
||||||
children: <Widget>[
|
child: ListView(
|
||||||
Padding(
|
children: <Widget>[
|
||||||
padding: const EdgeInsets.all(12.0),
|
Padding(
|
||||||
child: TextField(
|
padding: const EdgeInsets.all(12.0),
|
||||||
readOnly: controller.loading,
|
child: TextField(
|
||||||
autocorrect: false,
|
readOnly: controller.loading,
|
||||||
autofocus: true,
|
autocorrect: false,
|
||||||
onChanged: controller.checkWellKnownWithCoolDown,
|
autofocus: true,
|
||||||
controller: controller.usernameController,
|
onChanged: controller.checkWellKnownWithCoolDown,
|
||||||
textInputAction: TextInputAction.next,
|
controller: controller.usernameController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
textInputAction: TextInputAction.next,
|
||||||
autofillHints:
|
keyboardType: TextInputType.emailAddress,
|
||||||
controller.loading ? null : [AutofillHints.username],
|
autofillHints:
|
||||||
decoration: InputDecoration(
|
controller.loading ? null : [AutofillHints.username],
|
||||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
decoration: InputDecoration(
|
||||||
errorText: controller.usernameError,
|
prefixIcon: const Icon(Icons.account_box_outlined),
|
||||||
errorStyle: const TextStyle(color: Colors.orange),
|
errorText: controller.usernameError,
|
||||||
hintText: L10n.of(context)!.emailOrUsername,
|
errorStyle: const TextStyle(color: Colors.orange),
|
||||||
),
|
hintText: L10n.of(context)!.emailOrUsername,
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: TextField(
|
|
||||||
readOnly: controller.loading,
|
|
||||||
autocorrect: false,
|
|
||||||
autofillHints:
|
|
||||||
controller.loading ? null : [AutofillHints.password],
|
|
||||||
controller: controller.passwordController,
|
|
||||||
textInputAction: TextInputAction.go,
|
|
||||||
obscureText: !controller.showPassword,
|
|
||||||
onSubmitted: (_) => controller.login(),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.lock_outlined),
|
|
||||||
errorText: controller.passwordError,
|
|
||||||
errorStyle: const TextStyle(color: Colors.orange),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
onPressed: controller.toggleShowPassword,
|
|
||||||
icon: Icon(
|
|
||||||
controller.showPassword
|
|
||||||
? Icons.visibility_off_outlined
|
|
||||||
: Icons.visibility_outlined,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
hintText: L10n.of(context)!.password,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
Hero(
|
padding: const EdgeInsets.all(12.0),
|
||||||
tag: 'signinButton',
|
child: TextField(
|
||||||
child: Padding(
|
readOnly: controller.loading,
|
||||||
|
autocorrect: false,
|
||||||
|
autofillHints:
|
||||||
|
controller.loading ? null : [AutofillHints.password],
|
||||||
|
controller: controller.passwordController,
|
||||||
|
textInputAction: TextInputAction.go,
|
||||||
|
obscureText: !controller.showPassword,
|
||||||
|
onSubmitted: (_) => controller.login(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: const Icon(Icons.lock_outlined),
|
||||||
|
errorText: controller.passwordError,
|
||||||
|
errorStyle: const TextStyle(color: Colors.orange),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: controller.toggleShowPassword,
|
||||||
|
icon: Icon(
|
||||||
|
controller.showPassword
|
||||||
|
? Icons.visibility_off_outlined
|
||||||
|
: Icons.visibility_outlined,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hintText: L10n.of(context)!.password,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Hero(
|
||||||
|
tag: 'signinButton',
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
onPressed: controller.loading ? null : controller.login,
|
||||||
|
icon: const Icon(Icons.login_outlined),
|
||||||
|
label: controller.loading
|
||||||
|
? const LinearProgressIndicator()
|
||||||
|
: Text(L10n.of(context)!.login),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Divider(
|
||||||
|
thickness: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context)!.or,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Divider(
|
||||||
|
thickness: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: controller.loading
|
||||||
|
? () {}
|
||||||
|
: controller.passwordForgotten,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
backgroundColor: Theme.of(context).colorScheme.onError,
|
||||||
),
|
),
|
||||||
onPressed: controller.loading ? null : controller.login,
|
icon: const Icon(Icons.safety_check_outlined),
|
||||||
icon: const Icon(Icons.login_outlined),
|
label: Text(L10n.of(context)!.passwordForgotten),
|
||||||
label: controller.loading
|
|
||||||
? const LinearProgressIndicator()
|
|
||||||
: Text(L10n.of(context)!.login),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
);
|
||||||
child: Row(
|
},
|
||||||
children: [
|
),
|
||||||
Expanded(
|
|
||||||
child: Divider(
|
|
||||||
thickness: 1,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Text(
|
|
||||||
L10n.of(context)!.or,
|
|
||||||
style: const TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Divider(
|
|
||||||
thickness: 1,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed:
|
|
||||||
controller.loading ? () {} : controller.passwordForgotten,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.error,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.onError,
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.safety_check_outlined),
|
|
||||||
label: Text(L10n.of(context)!.passwordForgotten),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,10 @@ class NewGroupView extends StatelessWidget {
|
|||||||
textInputAction: TextInputAction.go,
|
textInputAction: TextInputAction.go,
|
||||||
onSubmitted: controller.submitAction,
|
onSubmitted: controller.submitAction,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: L10n.of(context)!.optionalGroupName,
|
labelText: L10n.of(context)!.optionalGroupName,
|
||||||
prefixIcon: const Icon(Icons.people_outlined),
|
prefixIcon: const Icon(Icons.people_outlined),
|
||||||
hintText: L10n.of(context)!.enterAGroupName),
|
hintText: L10n.of(context)!.enterAGroupName,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
|
@ -29,9 +29,10 @@ class NewSpaceView extends StatelessWidget {
|
|||||||
textInputAction: TextInputAction.go,
|
textInputAction: TextInputAction.go,
|
||||||
onSubmitted: controller.submitAction,
|
onSubmitted: controller.submitAction,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: L10n.of(context)!.spaceName,
|
labelText: L10n.of(context)!.spaceName,
|
||||||
prefixIcon: const Icon(Icons.people_outlined),
|
prefixIcon: const Icon(Icons.people_outlined),
|
||||||
hintText: L10n.of(context)!.enterASpacepName),
|
hintText: L10n.of(context)!.enterASpacepName,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
|
@ -40,98 +40,100 @@ class SettingsView extends StatelessWidget {
|
|||||||
key: const Key('SettingsListViewContent'),
|
key: const Key('SettingsListViewContent'),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FutureBuilder<Profile>(
|
FutureBuilder<Profile>(
|
||||||
future: controller.profileFuture,
|
future: controller.profileFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final profile = snapshot.data;
|
final profile = snapshot.data;
|
||||||
final mxid = Matrix.of(context).client.userID ??
|
final mxid =
|
||||||
L10n.of(context)!.user;
|
Matrix.of(context).client.userID ?? L10n.of(context)!.user;
|
||||||
final displayname =
|
final displayname =
|
||||||
profile?.displayName ?? mxid.localpart ?? mxid;
|
profile?.displayName ?? mxid.localpart ?? mxid;
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(32.0),
|
padding: const EdgeInsets.all(32.0),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Material(
|
Material(
|
||||||
elevation: Theme.of(context)
|
elevation: Theme.of(context)
|
||||||
.appBarTheme
|
.appBarTheme
|
||||||
.scrolledUnderElevation ??
|
.scrolledUnderElevation ??
|
||||||
4,
|
4,
|
||||||
shadowColor:
|
shadowColor:
|
||||||
Theme.of(context).appBarTheme.shadowColor,
|
Theme.of(context).appBarTheme.shadowColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
Avatar.defaultSize * 2.5),
|
|
||||||
),
|
),
|
||||||
child: Avatar(
|
borderRadius: BorderRadius.circular(
|
||||||
mxContent: profile?.avatarUrl,
|
Avatar.defaultSize * 2.5,
|
||||||
name: displayname,
|
|
||||||
size: Avatar.defaultSize * 2.5,
|
|
||||||
fontSize: 18 * 2.5,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (profile != null)
|
child: Avatar(
|
||||||
Positioned(
|
mxContent: profile?.avatarUrl,
|
||||||
bottom: 0,
|
name: displayname,
|
||||||
right: 0,
|
size: Avatar.defaultSize * 2.5,
|
||||||
child: FloatingActionButton.small(
|
fontSize: 18 * 2.5,
|
||||||
onPressed: controller.setAvatarAction,
|
),
|
||||||
heroTag: null,
|
),
|
||||||
child: const Icon(Icons.camera_alt_outlined),
|
if (profile != null)
|
||||||
),
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
onPressed: controller.setAvatarAction,
|
||||||
|
heroTag: null,
|
||||||
|
child: const Icon(Icons.camera_alt_outlined),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Column(
|
Expanded(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
TextButton.icon(
|
children: [
|
||||||
onPressed: controller.setDisplaynameAction,
|
TextButton.icon(
|
||||||
icon: const Icon(
|
onPressed: controller.setDisplaynameAction,
|
||||||
Icons.edit_outlined,
|
icon: const Icon(
|
||||||
size: 16,
|
Icons.edit_outlined,
|
||||||
),
|
size: 16,
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor:
|
|
||||||
Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
displayname,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
// style: const TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
style: TextButton.styleFrom(
|
||||||
onPressed: () => FluffyShare.share(mxid, context),
|
foregroundColor:
|
||||||
icon: const Icon(
|
Theme.of(context).colorScheme.onBackground,
|
||||||
Icons.copy_outlined,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor:
|
|
||||||
Theme.of(context).colorScheme.secondary,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
mxid,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
// style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
label: Text(
|
||||||
),
|
displayname,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
// style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => FluffyShare.share(mxid, context),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.copy_outlined,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
mxid,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
// style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(thickness: 1),
|
const Divider(thickness: 1),
|
||||||
if (showChatBackupBanner == null)
|
if (showChatBackupBanner == null)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -79,11 +79,12 @@ class Settings3PidController extends State<Settings3Pid> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final success = await showFutureLoadingDialog(
|
final success = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => Matrix.of(context).client.delete3pidFromAccount(
|
future: () => Matrix.of(context).client.delete3pidFromAccount(
|
||||||
identifier.address,
|
identifier.address,
|
||||||
identifier.medium,
|
identifier.medium,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
if (success.error != null) return;
|
if (success.error != null) return;
|
||||||
setState(() => request = null);
|
setState(() => request = null);
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,10 @@ class Settings3PidView extends StatelessWidget {
|
|||||||
body: MaxWidthBody(
|
body: MaxWidthBody(
|
||||||
child: FutureBuilder<List<ThirdPartyIdentifier>?>(
|
child: FutureBuilder<List<ThirdPartyIdentifier>?>(
|
||||||
future: controller.request,
|
future: controller.request,
|
||||||
builder: (BuildContext context,
|
builder: (
|
||||||
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot) {
|
BuildContext context,
|
||||||
|
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot,
|
||||||
|
) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -42,7 +44,8 @@ class Settings3PidView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final identifier = snapshot.data!;
|
final identifier = snapshot.data!;
|
||||||
return Column(
|
return Column(
|
||||||
@ -71,10 +74,11 @@ class Settings3PidView extends StatelessWidget {
|
|||||||
itemCount: identifier.length,
|
itemCount: identifier.length,
|
||||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
foregroundColor: Colors.grey,
|
foregroundColor: Colors.grey,
|
||||||
child: Icon(identifier[i].iconData)),
|
child: Icon(identifier[i].iconData),
|
||||||
|
),
|
||||||
title: Text(identifier[i].address),
|
title: Text(identifier[i].address),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
tooltip: L10n.of(context)!.delete,
|
tooltip: L10n.of(context)!.delete,
|
||||||
|
@ -60,13 +60,20 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => client.setRoomStateWithKey(
|
future: () => client.setRoomStateWithKey(
|
||||||
room!.id, 'im.ponies.room_emotes', stateKey ?? '', pack!.toJson()),
|
room!.id,
|
||||||
|
'im.ponies.room_emotes',
|
||||||
|
stateKey ?? '',
|
||||||
|
pack!.toJson(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => client.setAccountData(
|
future: () => client.setAccountData(
|
||||||
client.userID!, 'im.ponies.user_emotes', pack!.toJson()),
|
client.userID!,
|
||||||
|
'im.ponies.user_emotes',
|
||||||
|
pack!.toJson(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,7 +102,10 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => client.setAccountData(
|
future: () => client.setAccountData(
|
||||||
client.userID!, 'im.ponies.emote_rooms', content),
|
client.userID!,
|
||||||
|
'im.ponies.emote_rooms',
|
||||||
|
content,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
@ -197,7 +207,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void imagePickerAction(
|
void imagePickerAction(
|
||||||
ValueNotifier<ImagePackImageContent?> controller) async {
|
ValueNotifier<ImagePackImageContent?> controller,
|
||||||
|
) async {
|
||||||
final result =
|
final result =
|
||||||
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||||
if (result.fileName == null) return;
|
if (result.fileName == null) return;
|
||||||
|
@ -140,16 +140,17 @@ class EmotesSettingsView extends StatelessWidget {
|
|||||||
actions: !useShortCuts
|
actions: !useShortCuts
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
SubmitLineIntent:
|
SubmitLineIntent: CallbackAction(
|
||||||
CallbackAction(onInvoke: (i) {
|
onInvoke: (i) {
|
||||||
controller.submitImageAction(
|
controller.submitImageAction(
|
||||||
imageCode,
|
imageCode,
|
||||||
textEditingController.text,
|
textEditingController.text,
|
||||||
image,
|
image,
|
||||||
textEditingController,
|
textEditingController,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
child: TextField(
|
child: TextField(
|
||||||
readOnly: controller.readonly,
|
readOnly: controller.readonly,
|
||||||
|
@ -58,34 +58,36 @@ class SettingsIgnoreListView extends StatelessWidget {
|
|||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<Object>(
|
child: StreamBuilder<Object>(
|
||||||
stream: client.onAccountData.stream
|
stream: client.onAccountData.stream
|
||||||
.where((a) => a.type == 'm.ignored_user_list'),
|
.where((a) => a.type == 'm.ignored_user_list'),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: client.ignoredUsers.length,
|
itemCount: client.ignoredUsers.length,
|
||||||
itemBuilder: (c, i) => FutureBuilder<Profile>(
|
itemBuilder: (c, i) => FutureBuilder<Profile>(
|
||||||
future:
|
future:
|
||||||
client.getProfileFromUserId(client.ignoredUsers[i]),
|
client.getProfileFromUserId(client.ignoredUsers[i]),
|
||||||
builder: (c, s) => ListTile(
|
builder: (c, s) => ListTile(
|
||||||
leading: Avatar(
|
leading: Avatar(
|
||||||
mxContent: s.data?.avatarUrl ?? Uri.parse(''),
|
mxContent: s.data?.avatarUrl ?? Uri.parse(''),
|
||||||
name: s.data?.displayName ?? client.ignoredUsers[i],
|
name: s.data?.displayName ?? client.ignoredUsers[i],
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
s.data?.displayName ?? client.ignoredUsers[i]),
|
s.data?.displayName ?? client.ignoredUsers[i],
|
||||||
trailing: IconButton(
|
),
|
||||||
tooltip: L10n.of(context)!.delete,
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.delete_forever_outlined),
|
tooltip: L10n.of(context)!.delete,
|
||||||
onPressed: () => showFutureLoadingDialog(
|
icon: const Icon(Icons.delete_forever_outlined),
|
||||||
context: context,
|
onPressed: () => showFutureLoadingDialog(
|
||||||
future: () =>
|
context: context,
|
||||||
client.unignoreUser(client.ignoredUsers[i]),
|
future: () =>
|
||||||
),
|
client.unignoreUser(client.ignoredUsers[i]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -32,27 +32,28 @@ class MultipleEmotesSettingsView extends StatelessWidget {
|
|||||||
final keys = packs.keys.toList();
|
final keys = packs.keys.toList();
|
||||||
keys.sort();
|
keys.sort();
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
separatorBuilder: (BuildContext context, int i) => Container(),
|
separatorBuilder: (BuildContext context, int i) => Container(),
|
||||||
itemCount: keys.length,
|
itemCount: keys.length,
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
final event = packs[keys[i]];
|
final event = packs[keys[i]];
|
||||||
String? packName =
|
String? packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
||||||
keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
if (event != null && event.content['pack'] is Map) {
|
||||||
if (event != null && event.content['pack'] is Map) {
|
if (event.content['pack']['displayname'] is String) {
|
||||||
if (event.content['pack']['displayname'] is String) {
|
packName = event.content['pack']['displayname'];
|
||||||
packName = event.content['pack']['displayname'];
|
} else if (event.content['pack']['name'] is String) {
|
||||||
} else if (event.content['pack']['name'] is String) {
|
packName = event.content['pack']['name'];
|
||||||
packName = event.content['pack']['name'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ListTile(
|
}
|
||||||
title: Text(packName!),
|
return ListTile(
|
||||||
onTap: () async {
|
title: Text(packName!),
|
||||||
VRouter.of(context).toSegments(
|
onTap: () async {
|
||||||
['rooms', room.id, 'details', 'emotes', keys[i]]);
|
VRouter.of(context).toSegments(
|
||||||
},
|
['rooms', room.id, 'details', 'emotes', keys[i]],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -25,90 +25,94 @@ class SettingsNotificationsView extends StatelessWidget {
|
|||||||
body: MaxWidthBody(
|
body: MaxWidthBody(
|
||||||
withScrolling: true,
|
withScrolling: true,
|
||||||
child: StreamBuilder(
|
child: StreamBuilder(
|
||||||
stream: Matrix.of(context)
|
stream: Matrix.of(context)
|
||||||
.client
|
.client
|
||||||
.onAccountData
|
.onAccountData
|
||||||
.stream
|
.stream
|
||||||
.where((event) => event.type == 'm.push_rules'),
|
.where((event) => event.type == 'm.push_rules'),
|
||||||
builder: (BuildContext context, _) {
|
builder: (BuildContext context, _) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
value: !Matrix.of(context).client.allPushNotificationsMuted,
|
value: !Matrix.of(context).client.allPushNotificationsMuted,
|
||||||
title: Text(
|
title: Text(
|
||||||
L10n.of(context)!.notificationsEnabledForThisAccount),
|
L10n.of(context)!.notificationsEnabledForThisAccount,
|
||||||
onChanged: (_) => showFutureLoadingDialog(
|
|
||||||
context: context,
|
|
||||||
future: () =>
|
|
||||||
Matrix.of(context).client.setMuteAllPushNotifications(
|
|
||||||
!Matrix.of(context)
|
|
||||||
.client
|
|
||||||
.allPushNotificationsMuted,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
|
onChanged: (_) => showFutureLoadingDialog(
|
||||||
const Divider(thickness: 1),
|
context: context,
|
||||||
ListTile(
|
future: () => Matrix.of(context)
|
||||||
title: Text(
|
.client
|
||||||
L10n.of(context)!.pushRules,
|
.setMuteAllPushNotifications(
|
||||||
style: TextStyle(
|
!Matrix.of(context).client.allPushNotificationsMuted,
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (var item in NotificationSettingsItem.items)
|
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
|
||||||
SwitchListTile.adaptive(
|
|
||||||
value: controller.getNotificationSetting(item) ?? true,
|
|
||||||
title: Text(item.title(context)),
|
|
||||||
onChanged: (bool enabled) =>
|
|
||||||
controller.setNotificationSetting(item, enabled),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
const Divider(thickness: 1),
|
const Divider(thickness: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
L10n.of(context)!.devices,
|
L10n.of(context)!.pushRules,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
FutureBuilder<List<Pusher>?>(
|
for (var item in NotificationSettingsItem.items)
|
||||||
future: controller.pusherFuture ??=
|
SwitchListTile.adaptive(
|
||||||
Matrix.of(context).client.getPushers(),
|
value: controller.getNotificationSetting(item) ?? true,
|
||||||
builder: (context, snapshot) {
|
title: Text(item.title(context)),
|
||||||
if (snapshot.hasError) {
|
onChanged: (bool enabled) =>
|
||||||
Center(
|
controller.setNotificationSetting(item, enabled),
|
||||||
child: Text(
|
),
|
||||||
snapshot.error!.toLocalizedString(context),
|
},
|
||||||
),
|
const Divider(thickness: 1),
|
||||||
);
|
ListTile(
|
||||||
}
|
title: Text(
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
L10n.of(context)!.devices,
|
||||||
const Center(
|
style: TextStyle(
|
||||||
child: CircularProgressIndicator.adaptive(
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
strokeWidth: 2));
|
fontWeight: FontWeight.bold,
|
||||||
}
|
),
|
||||||
final pushers = snapshot.data ?? [];
|
),
|
||||||
return ListView.builder(
|
),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
FutureBuilder<List<Pusher>?>(
|
||||||
shrinkWrap: true,
|
future: controller.pusherFuture ??=
|
||||||
itemCount: pushers.length,
|
Matrix.of(context).client.getPushers(),
|
||||||
itemBuilder: (_, i) => ListTile(
|
builder: (context, snapshot) {
|
||||||
title: Text(
|
if (snapshot.hasError) {
|
||||||
'${pushers[i].appDisplayName} - ${pushers[i].appId}'),
|
Center(
|
||||||
subtitle: Text(pushers[i].data.url.toString()),
|
child: Text(
|
||||||
onTap: () => controller.onPusherTap(pushers[i]),
|
snapshot.error!.toLocalizedString(context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
],
|
const Center(
|
||||||
);
|
child: CircularProgressIndicator.adaptive(
|
||||||
}),
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final pushers = snapshot.data ?? [];
|
||||||
|
return ListView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: pushers.length,
|
||||||
|
itemBuilder: (_, i) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
'${pushers[i].appDisplayName} - ${pushers[i].appId}',
|
||||||
|
),
|
||||||
|
subtitle: Text(pushers[i].data.url.toString()),
|
||||||
|
onTap: () => controller.onPusherTap(pushers[i]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,8 @@ class SettingsSecurityController extends State<SettingsSecurity> {
|
|||||||
);
|
);
|
||||||
if (success.error == null) {
|
if (success.error == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)));
|
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +152,8 @@ class SettingsSecurityController extends State<SettingsSecurity> {
|
|||||||
auth: AuthenticationPassword(
|
auth: AuthenticationPassword(
|
||||||
password: input.single,
|
password: input.single,
|
||||||
identifier: AuthenticationUserIdentifier(
|
identifier: AuthenticationUserIdentifier(
|
||||||
user: Matrix.of(context).client.userID!),
|
user: Matrix.of(context).client.userID!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -181,10 +183,11 @@ class SettingsSecurityController extends State<SettingsSecurity> {
|
|||||||
try {
|
try {
|
||||||
final export = await Matrix.of(context).client.exportDump();
|
final export = await Matrix.of(context).client.exportDump();
|
||||||
final filePickerCross = FilePickerCross(
|
final filePickerCross = FilePickerCross(
|
||||||
Uint8List.fromList(const Utf8Codec().encode(export!)),
|
Uint8List.fromList(const Utf8Codec().encode(export!)),
|
||||||
path:
|
path:
|
||||||
'/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup',
|
'/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup',
|
||||||
fileExtension: 'fluffybackup');
|
fileExtension: 'fluffybackup',
|
||||||
|
);
|
||||||
await filePickerCross.exportToStorage(
|
await filePickerCross.exportToStorage(
|
||||||
subject: L10n.of(context)!.dehydrateShare,
|
subject: L10n.of(context)!.dehydrateShare,
|
||||||
);
|
);
|
||||||
|
@ -32,14 +32,15 @@ class SettingsStoriesController extends State<SettingsStories> {
|
|||||||
final blockList = room.client.storiesBlockList;
|
final blockList = room.client.storiesBlockList;
|
||||||
blockList.add(user.id);
|
blockList.add(user.id);
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
await user.kick();
|
await user.kick();
|
||||||
await room.client.setStoriesBlockList(blockList.toSet().toList());
|
await room.client.setStoriesBlockList(blockList.toSet().toList());
|
||||||
setState(() {
|
setState(() {
|
||||||
users[user] = false;
|
users[user] = false;
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,14 +48,15 @@ class SettingsStoriesController extends State<SettingsStories> {
|
|||||||
final blockList = room.client.storiesBlockList;
|
final blockList = room.client.storiesBlockList;
|
||||||
blockList.remove(user.id);
|
blockList.remove(user.id);
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
await room.client.setStoriesBlockList(blockList);
|
await room.client.setStoriesBlockList(blockList);
|
||||||
await room.invite(user.id);
|
await room.invite(user.id);
|
||||||
setState(() {
|
setState(() {
|
||||||
users[user] = true;
|
users[user] = true;
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,9 +38,10 @@ class SettingsStoriesView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: controller.users.length,
|
itemCount: controller.users.length,
|
||||||
|
@ -59,16 +59,18 @@ class SettingsStyleView extends StatelessWidget {
|
|||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(colorPickerSize),
|
BorderRadius.circular(colorPickerSize),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: colorPickerSize,
|
width: colorPickerSize,
|
||||||
height: colorPickerSize,
|
height: colorPickerSize,
|
||||||
child: controller.currentColor == color
|
child: controller.currentColor == color
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
))
|
),
|
||||||
: null),
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -118,16 +120,18 @@ class SettingsStyleView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: controller.deleteWallpaperAction,
|
onTap: controller.deleteWallpaperAction,
|
||||||
),
|
),
|
||||||
Builder(builder: (context) {
|
Builder(
|
||||||
return ListTile(
|
builder: (context) {
|
||||||
title: Text(L10n.of(context)!.changeWallpaper),
|
return ListTile(
|
||||||
trailing: Icon(
|
title: Text(L10n.of(context)!.changeWallpaper),
|
||||||
Icons.photo_outlined,
|
trailing: Icon(
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
Icons.photo_outlined,
|
||||||
),
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
onTap: controller.setWallpaperAction,
|
),
|
||||||
);
|
onTap: controller.setWallpaperAction,
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
@ -46,10 +46,11 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
Timeline? timeline;
|
Timeline? timeline;
|
||||||
|
|
||||||
Event? get currentEvent => index < events.length ? events[index] : null;
|
Event? get currentEvent => index < events.length ? events[index] : null;
|
||||||
StoryThemeData get storyThemeData =>
|
StoryThemeData get storyThemeData => StoryThemeData.fromJson(
|
||||||
StoryThemeData.fromJson(currentEvent?.content
|
currentEvent?.content
|
||||||
.tryGetMap<String, dynamic>(StoryThemeData.contentKey) ??
|
.tryGetMap<String, dynamic>(StoryThemeData.contentKey) ??
|
||||||
{});
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
bool replyLoading = false;
|
bool replyLoading = false;
|
||||||
bool _modalOpened = false;
|
bool _modalOpened = false;
|
||||||
@ -83,8 +84,9 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
final client = Matrix.of(context).client;
|
final client = Matrix.of(context).client;
|
||||||
final roomId = await client.startDirectChat(currentEvent.senderId);
|
final roomId = await client.startDirectChat(currentEvent.senderId);
|
||||||
var replyText = L10n.of(context)!.storyFrom(
|
var replyText = L10n.of(context)!.storyFrom(
|
||||||
currentEvent.originServerTs.localizedTime(context),
|
currentEvent.originServerTs.localizedTime(context),
|
||||||
currentEvent.content.tryGet<String>('body') ?? '');
|
currentEvent.content.tryGet<String>('body') ?? '',
|
||||||
|
);
|
||||||
replyText = replyText.split('\n').map((line) => '> $line').join('\n');
|
replyText = replyText.split('\n').map((line) => '> $line').join('\n');
|
||||||
message = '$replyText\n\n$message';
|
message = '$replyText\n\n$message';
|
||||||
await client.getRoomById(roomId)!.sendTextEvent(message);
|
await client.getRoomById(roomId)!.sendTextEvent(message);
|
||||||
@ -307,33 +309,35 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
final event = currentEvent;
|
final event = currentEvent;
|
||||||
if (event == null) return;
|
if (event == null) return;
|
||||||
final score = await showConfirmationDialog<int>(
|
final score = await showConfirmationDialog<int>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.reportMessage,
|
title: L10n.of(context)!.reportMessage,
|
||||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
actions: [
|
actions: [
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -100,
|
key: -100,
|
||||||
label: L10n.of(context)!.extremeOffensive,
|
label: L10n.of(context)!.extremeOffensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -50,
|
key: -50,
|
||||||
label: L10n.of(context)!.offensive,
|
label: L10n.of(context)!.offensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: 0,
|
key: 0,
|
||||||
label: L10n.of(context)!.inoffensive,
|
label: L10n.of(context)!.inoffensive,
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
if (score == null) return;
|
if (score == null) return;
|
||||||
final reason = await showTextInputDialog(
|
final reason = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||||
|
);
|
||||||
if (reason == null || reason.single.isEmpty) return;
|
if (reason == null || reason.single.isEmpty) return;
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -352,7 +356,9 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<MatrixFile> downloadAndDecryptAttachment(
|
Future<MatrixFile> downloadAndDecryptAttachment(
|
||||||
Event event, bool getThumbnail) async {
|
Event event,
|
||||||
|
bool getThumbnail,
|
||||||
|
) async {
|
||||||
return _fileCache[event.eventId] ??=
|
return _fileCache[event.eventId] ??=
|
||||||
event.downloadAndDecryptAttachment(getThumbnail: getThumbnail);
|
event.downloadAndDecryptAttachment(getThumbnail: getThumbnail);
|
||||||
}
|
}
|
||||||
@ -400,10 +406,12 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
final timeline = this.timeline = await room.getTimeline();
|
final timeline = this.timeline = await room.getTimeline();
|
||||||
timeline.requestKeys();
|
timeline.requestKeys();
|
||||||
var events = timeline.events
|
var events = timeline.events
|
||||||
.where((e) =>
|
.where(
|
||||||
e.type == EventTypes.Message &&
|
(e) =>
|
||||||
!e.redacted &&
|
e.type == EventTypes.Message &&
|
||||||
e.status == EventStatus.synced)
|
!e.redacted &&
|
||||||
|
e.status == EventStatus.synced,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final hasOutdatedEvents = events.removeOutdatedEvents();
|
final hasOutdatedEvents = events.removeOutdatedEvents();
|
||||||
@ -432,12 +440,16 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
|
|
||||||
// Preload images and videos
|
// Preload images and videos
|
||||||
events
|
events
|
||||||
.where((event) => {MessageTypes.Image, MessageTypes.Video}
|
.where(
|
||||||
.contains(event.messageType))
|
(event) => {MessageTypes.Image, MessageTypes.Video}
|
||||||
.forEach((event) => downloadAndDecryptAttachment(
|
.contains(event.messageType),
|
||||||
|
)
|
||||||
|
.forEach(
|
||||||
|
(event) => downloadAndDecryptAttachment(
|
||||||
event,
|
event,
|
||||||
event.messageType == MessageTypes.Video &&
|
event.messageType == MessageTypes.Video && PlatformInfos.isMobile,
|
||||||
PlatformInfos.isMobile));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Reverse list
|
// Reverse list
|
||||||
this.events.clear();
|
this.events.clear();
|
||||||
@ -502,9 +514,11 @@ class StoryPageController extends State<StoryPage> {
|
|||||||
|
|
||||||
extension on List<Event> {
|
extension on List<Event> {
|
||||||
bool removeOutdatedEvents() {
|
bool removeOutdatedEvents() {
|
||||||
final outdatedIndex = indexWhere((event) =>
|
final outdatedIndex = indexWhere(
|
||||||
DateTime.now().difference(event.originServerTs).inHours >
|
(event) =>
|
||||||
ClientStoriesExtension.lifeTimeInHours);
|
DateTime.now().difference(event.originServerTs).inHours >
|
||||||
|
ClientStoriesExtension.lifeTimeInHours,
|
||||||
|
);
|
||||||
if (outdatedIndex != -1) {
|
if (outdatedIndex != -1) {
|
||||||
removeRange(outdatedIndex, length);
|
removeRange(outdatedIndex, length);
|
||||||
return true;
|
return true;
|
||||||
|
@ -144,9 +144,10 @@ class StoryView extends StatelessWidget {
|
|||||||
final events = controller.events;
|
final events = controller.events;
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (events.isEmpty) {
|
if (events.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@ -218,7 +219,9 @@ class StoryView extends StatelessWidget {
|
|||||||
!PlatformInfos.isMobile))
|
!PlatformInfos.isMobile))
|
||||||
FutureBuilder<MatrixFile>(
|
FutureBuilder<MatrixFile>(
|
||||||
future: controller.downloadAndDecryptAttachment(
|
future: controller.downloadAndDecryptAttachment(
|
||||||
event, event.messageType == MessageTypes.Video),
|
event,
|
||||||
|
event.messageType == MessageTypes.Video,
|
||||||
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final matrixFile = snapshot.data;
|
final matrixFile = snapshot.data;
|
||||||
if (matrixFile == null) {
|
if (matrixFile == null) {
|
||||||
@ -364,7 +367,8 @@ class StoryView extends StatelessWidget {
|
|||||||
height: 16,
|
height: 16,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2),
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: IconButton(
|
: IconButton(
|
||||||
|
@ -52,33 +52,35 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
|||||||
case UserBottomSheetAction.report:
|
case UserBottomSheetAction.report:
|
||||||
final event = widget.user;
|
final event = widget.user;
|
||||||
final score = await showConfirmationDialog<int>(
|
final score = await showConfirmationDialog<int>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.reportUser,
|
title: L10n.of(context)!.reportUser,
|
||||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
actions: [
|
actions: [
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -100,
|
key: -100,
|
||||||
label: L10n.of(context)!.extremeOffensive,
|
label: L10n.of(context)!.extremeOffensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -50,
|
key: -50,
|
||||||
label: L10n.of(context)!.offensive,
|
label: L10n.of(context)!.offensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: 0,
|
key: 0,
|
||||||
label: L10n.of(context)!.inoffensive,
|
label: L10n.of(context)!.inoffensive,
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
if (score == null) return;
|
if (score == null) return;
|
||||||
final reason = await showTextInputDialog(
|
final reason = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||||
|
);
|
||||||
if (reason == null || reason.single.isEmpty) return;
|
if (reason == null || reason.single.isEmpty) return;
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -91,7 +93,8 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
|||||||
);
|
);
|
||||||
if (result.error != null) return;
|
if (result.error != null) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)));
|
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case UserBottomSheetAction.mention:
|
case UserBottomSheetAction.mention:
|
||||||
Navigator.of(context, rootNavigator: false).pop();
|
Navigator.of(context, rootNavigator: false).pop();
|
||||||
@ -151,9 +154,9 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
|||||||
case UserBottomSheetAction.ignore:
|
case UserBottomSheetAction.ignore:
|
||||||
if (await askConfirmation()) {
|
if (await askConfirmation()) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () =>
|
future: () => Matrix.of(context).client.ignoreUser(widget.user.id),
|
||||||
Matrix.of(context).client.ignoreUser(widget.user.id));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,10 +55,12 @@ extension AccountBundlesExtension on Client {
|
|||||||
}
|
}
|
||||||
ret ??= [];
|
ret ??= [];
|
||||||
if (ret.isEmpty) {
|
if (ret.isEmpty) {
|
||||||
ret.add(AccountBundle(
|
ret.add(
|
||||||
name: userID,
|
AccountBundle(
|
||||||
priority: 0,
|
name: userID,
|
||||||
));
|
priority: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,8 @@ class BackgroundPush {
|
|||||||
firebase?.setListeners(
|
firebase?.setListeners(
|
||||||
onMessage: (message) => pushHelper(
|
onMessage: (message) => pushHelper(
|
||||||
PushNotification.fromJson(
|
PushNotification.fromJson(
|
||||||
Map<String, dynamic>.from(message['data'] ?? message)),
|
Map<String, dynamic>.from(message['data'] ?? message),
|
||||||
|
),
|
||||||
client: client,
|
client: client,
|
||||||
l10n: l10n,
|
l10n: l10n,
|
||||||
activeRoomId: router?.currentState?.pathParameters['roomid'],
|
activeRoomId: router?.currentState?.pathParameters['roomid'],
|
||||||
@ -331,7 +332,8 @@ class BackgroundPush {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logs().i(
|
Logs().i(
|
||||||
'[Push] No self-hosted unified push gateway present: $newEndpoint');
|
'[Push] No self-hosted unified push gateway present: $newEndpoint',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Logs().i('[Push] UnifiedPush using endpoint $endpoint');
|
Logs().i('[Push] UnifiedPush using endpoint $endpoint');
|
||||||
final oldTokens = <String?>{};
|
final oldTokens = <String?>{};
|
||||||
@ -366,7 +368,8 @@ class BackgroundPush {
|
|||||||
Future<void> _onUpMessage(Uint8List message, String i) async {
|
Future<void> _onUpMessage(Uint8List message, String i) async {
|
||||||
upAction = true;
|
upAction = true;
|
||||||
final data = Map<String, dynamic>.from(
|
final data = Map<String, dynamic>.from(
|
||||||
json.decode(utf8.decode(message))['notification']);
|
json.decode(utf8.decode(message))['notification'],
|
||||||
|
);
|
||||||
// UP may strip the devices list
|
// UP may strip the devices list
|
||||||
data['devices'] ??= [];
|
data['devices'] ??= [];
|
||||||
await pushHelper(
|
await pushHelper(
|
||||||
@ -382,8 +385,11 @@ class BackgroundPush {
|
|||||||
/// IDs we map the [roomId] to a number and store this number.
|
/// IDs we map the [roomId] to a number and store this number.
|
||||||
late Map<String, int> idMap;
|
late Map<String, int> idMap;
|
||||||
Future<void> _loadIdMap() async {
|
Future<void> _loadIdMap() async {
|
||||||
idMap = Map<String, int>.from(json.decode(
|
idMap = Map<String, int>.from(
|
||||||
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}'));
|
json.decode(
|
||||||
|
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> mapRoomIdToInt(String roomId) async {
|
Future<int> mapRoomIdToInt(String roomId) async {
|
||||||
@ -441,7 +447,8 @@ class BackgroundPush {
|
|||||||
if (syncErrored) {
|
if (syncErrored) {
|
||||||
try {
|
try {
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'[Push] failed to sync for fallback push, fetching notifications endpoint...');
|
'[Push] failed to sync for fallback push, fetching notifications endpoint...',
|
||||||
|
);
|
||||||
final notifications = await client.getNotifications(limit: 20);
|
final notifications = await client.getNotifications(limit: 20);
|
||||||
final notificationRooms =
|
final notificationRooms =
|
||||||
notifications.notifications.map((n) => n.roomId).toSet();
|
notifications.notifications.map((n) => n.roomId).toSet();
|
||||||
@ -450,8 +457,9 @@ class BackgroundPush {
|
|||||||
.map((r) => r.id);
|
.map((r) => r.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'[Push] failed to fetch pending notifications for clearing push, falling back...',
|
'[Push] failed to fetch pending notifications for clearing push, falling back...',
|
||||||
e);
|
e,
|
||||||
|
);
|
||||||
emptyRooms = client.rooms
|
emptyRooms = client.rooms
|
||||||
.where((r) => r.notificationCount == 0)
|
.where((r) => r.notificationCount == 0)
|
||||||
.map((r) => r.id);
|
.map((r) => r.id);
|
||||||
@ -474,7 +482,9 @@ class BackgroundPush {
|
|||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
await store.setItem(
|
await store.setItem(
|
||||||
SettingKeys.notificationCurrentIds, json.encode(idMap));
|
SettingKeys.notificationCurrentIds,
|
||||||
|
json.encode(idMap),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_clearingPushLock = false;
|
_clearingPushLock = false;
|
||||||
|
@ -39,19 +39,25 @@ abstract class ClientManager {
|
|||||||
}
|
}
|
||||||
final clients = clientNames.map(createClient).toList();
|
final clients = clientNames.map(createClient).toList();
|
||||||
if (initialize) {
|
if (initialize) {
|
||||||
await Future.wait(clients.map((client) => client
|
await Future.wait(
|
||||||
.init(
|
clients.map(
|
||||||
waitForFirstSync: false,
|
(client) => client
|
||||||
waitUntilLoadCompletedLoaded: false,
|
.init(
|
||||||
)
|
waitForFirstSync: false,
|
||||||
.catchError(
|
waitUntilLoadCompletedLoaded: false,
|
||||||
(e, s) => Logs().e('Unable to initialize client', e, s))));
|
)
|
||||||
|
.catchError(
|
||||||
|
(e, s) => Logs().e('Unable to initialize client', e, s),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (clients.length > 1 && clients.any((c) => !c.isLogged())) {
|
if (clients.length > 1 && clients.any((c) => !c.isLogged())) {
|
||||||
final loggedOutClients = clients.where((c) => !c.isLogged()).toList();
|
final loggedOutClients = clients.where((c) => !c.isLogged()).toList();
|
||||||
for (final client in loggedOutClients) {
|
for (final client in loggedOutClients) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'Multi account is enabled but client ${client.userID} is not logged in. Removing...');
|
'Multi account is enabled but client ${client.userID} is not logged in. Removing...',
|
||||||
|
);
|
||||||
clientNames.remove(client.clientName);
|
clientNames.remove(client.clientName);
|
||||||
clients.remove(client);
|
clients.remove(client);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ import 'package:matrix/matrix.dart';
|
|||||||
import 'package:native_imaging/native_imaging.dart' as native;
|
import 'package:native_imaging/native_imaging.dart' as native;
|
||||||
|
|
||||||
Future<MatrixImageFileResizedResponse?> customImageResizer(
|
Future<MatrixImageFileResizedResponse?> customImageResizer(
|
||||||
MatrixImageFileResizeArguments arguments) async {
|
MatrixImageFileResizeArguments arguments,
|
||||||
|
) async {
|
||||||
await native.init();
|
await native.init();
|
||||||
late native.Image nativeImg;
|
late native.Image nativeImg;
|
||||||
|
|
||||||
@ -21,7 +22,10 @@ Future<MatrixImageFileResizedResponse?> customImageResizer(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final rgba = Uint8List.view(
|
final rgba = Uint8List.view(
|
||||||
rgbaData.buffer, rgbaData.offsetInBytes, rgbaData.lengthInBytes);
|
rgbaData.buffer,
|
||||||
|
rgbaData.offsetInBytes,
|
||||||
|
rgbaData.lengthInBytes,
|
||||||
|
);
|
||||||
|
|
||||||
final width = dartFrame.image.width;
|
final width = dartFrame.image.width;
|
||||||
final height = dartFrame.image.height;
|
final height = dartFrame.image.height;
|
||||||
|
@ -77,10 +77,15 @@ extension DateTimeExtension on DateTime {
|
|||||||
}
|
}
|
||||||
} else if (sameYear) {
|
} else if (sameYear) {
|
||||||
return L10n.of(context)!.dateWithoutYear(
|
return L10n.of(context)!.dateWithoutYear(
|
||||||
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0'));
|
month.toString().padLeft(2, '0'),
|
||||||
|
day.toString().padLeft(2, '0'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return L10n.of(context)!.dateWithYear(year.toString(),
|
return L10n.of(context)!.dateWithYear(
|
||||||
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0'));
|
year.toString(),
|
||||||
|
month.toString().padLeft(2, '0'),
|
||||||
|
day.toString().padLeft(2, '0'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the DateTime is today, this returns [localizedTimeOfDay()], if not it also
|
/// If the DateTime is today, this returns [localizedTimeOfDay()], if not it also
|
||||||
@ -95,7 +100,9 @@ extension DateTimeExtension on DateTime {
|
|||||||
|
|
||||||
if (sameDay) return localizedTimeOfDay(context);
|
if (sameDay) return localizedTimeOfDay(context);
|
||||||
return L10n.of(context)!.dateAndTimeOfDay(
|
return L10n.of(context)!.dateAndTimeOfDay(
|
||||||
localizedTimeShort(context), localizedTimeOfDay(context));
|
localizedTimeShort(context),
|
||||||
|
localizedTimeOfDay(context),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString();
|
static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString();
|
||||||
|
@ -19,7 +19,8 @@ abstract class FluffyShare {
|
|||||||
ClipboardData(text: text),
|
ClipboardData(text: text),
|
||||||
);
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)));
|
SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,10 @@ extension ClientStoriesExtension on Client {
|
|||||||
|
|
||||||
List<User> get contacts => rooms
|
List<User> get contacts => rooms
|
||||||
.where((room) => room.isDirectChat)
|
.where((room) => room.isDirectChat)
|
||||||
.map((room) =>
|
.map(
|
||||||
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!))
|
(room) =>
|
||||||
|
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<Room> get storiesRooms =>
|
List<Room> get storiesRooms =>
|
||||||
@ -78,23 +80,30 @@ extension ClientStoriesExtension on Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Room?> getStoriesRoom(BuildContext context) async {
|
Future<Room?> getStoriesRoom(BuildContext context) async {
|
||||||
final candidates = rooms.where((room) =>
|
final candidates = rooms.where(
|
||||||
room.getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') ==
|
(room) =>
|
||||||
storiesRoomType &&
|
room
|
||||||
room.ownPowerLevel >= 100);
|
.getState(EventTypes.RoomCreate)
|
||||||
|
?.content
|
||||||
|
.tryGet<String>('type') ==
|
||||||
|
storiesRoomType &&
|
||||||
|
room.ownPowerLevel >= 100,
|
||||||
|
);
|
||||||
if (candidates.isEmpty) return null;
|
if (candidates.isEmpty) return null;
|
||||||
if (candidates.length == 1) return candidates.single;
|
if (candidates.length == 1) return candidates.single;
|
||||||
return await showModalActionSheet<Room>(
|
return await showModalActionSheet<Room>(
|
||||||
context: context,
|
context: context,
|
||||||
actions: candidates
|
actions: candidates
|
||||||
.map(
|
.map(
|
||||||
(room) => SheetAction(
|
(room) => SheetAction(
|
||||||
label: room.getLocalizedDisplayname(
|
label: room.getLocalizedDisplayname(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
),
|
),
|
||||||
key: room),
|
key: room,
|
||||||
)
|
),
|
||||||
.toList());
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,8 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
|
|||||||
static const String _cipherStorageKey = 'hive_encryption_key';
|
static const String _cipherStorageKey = 'hive_encryption_key';
|
||||||
|
|
||||||
static Future<FlutterHiveCollectionsDatabase> databaseBuilder(
|
static Future<FlutterHiveCollectionsDatabase> databaseBuilder(
|
||||||
Client client) async {
|
Client client,
|
||||||
|
) async {
|
||||||
Logs().d('Open Hive...');
|
Logs().d('Open Hive...');
|
||||||
HiveAesCipher? hiverCipher;
|
HiveAesCipher? hiverCipher;
|
||||||
try {
|
try {
|
||||||
@ -96,9 +97,9 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// do not destroy your stable FluffyChat in debug mode
|
// do not destroy your stable FluffyChat in debug mode
|
||||||
directory = Directory(directory.uri
|
directory = Directory(
|
||||||
.resolve(kDebugMode ? 'hive_debug' : 'hive')
|
directory.uri.resolve(kDebugMode ? 'hive_debug' : 'hive').toFilePath(),
|
||||||
.toFilePath());
|
);
|
||||||
directory.create(recursive: true);
|
directory.create(recursive: true);
|
||||||
path = directory.path;
|
path = directory.path;
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,9 @@ extension IosBadgeClientExtension on Client {
|
|||||||
void updateIosBadge() {
|
void updateIosBadge() {
|
||||||
if (PlatformInfos.isIOS) {
|
if (PlatformInfos.isIOS) {
|
||||||
// Workaround for iOS not clearing notifications with fcm_shared_isolate
|
// Workaround for iOS not clearing notifications with fcm_shared_isolate
|
||||||
if (!rooms.any((r) =>
|
if (!rooms.any(
|
||||||
r.membership == Membership.invite || (r.notificationCount > 0))) {
|
(r) => r.membership == Membership.invite || (r.notificationCount > 0),
|
||||||
|
)) {
|
||||||
// ignore: unawaited_futures
|
// ignore: unawaited_futures
|
||||||
FlutterLocalNotificationsPlugin().cancelAll();
|
FlutterLocalNotificationsPlugin().cancelAll();
|
||||||
FlutterAppBadger.removeBadge();
|
FlutterAppBadger.removeBadge();
|
||||||
|
@ -62,7 +62,9 @@ class MatrixLocals extends MatrixLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String changedTheGuestAccessRulesTo(
|
String changedTheGuestAccessRulesTo(
|
||||||
String senderName, String localizedString) {
|
String senderName,
|
||||||
|
String localizedString,
|
||||||
|
) {
|
||||||
return l10n.changedTheGuestAccessRulesTo(senderName, localizedString);
|
return l10n.changedTheGuestAccessRulesTo(senderName, localizedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +75,9 @@ class MatrixLocals extends MatrixLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String changedTheHistoryVisibilityTo(
|
String changedTheHistoryVisibilityTo(
|
||||||
String senderName, String localizedString) {
|
String senderName,
|
||||||
|
String localizedString,
|
||||||
|
) {
|
||||||
return l10n.changedTheHistoryVisibilityTo(senderName, localizedString);
|
return l10n.changedTheHistoryVisibilityTo(senderName, localizedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,9 @@ Future<void> _tryPushHelper(
|
|||||||
await flutterLocalNotificationsPlugin.cancelAll();
|
await flutterLocalNotificationsPlugin.cancelAll();
|
||||||
final store = await SharedPreferences.getInstance();
|
final store = await SharedPreferences.getInstance();
|
||||||
await store.setString(
|
await store.setString(
|
||||||
SettingKeys.notificationCurrentIds, json.encode({}));
|
SettingKeys.notificationCurrentIds,
|
||||||
|
json.encode({}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -237,7 +239,8 @@ Future<void> _tryPushHelper(
|
|||||||
Future<int> mapRoomIdToInt(String roomId) async {
|
Future<int> mapRoomIdToInt(String roomId) async {
|
||||||
final store = await SharedPreferences.getInstance();
|
final store = await SharedPreferences.getInstance();
|
||||||
final idMap = Map<String, int>.from(
|
final idMap = Map<String, int>.from(
|
||||||
jsonDecode(store.getString(SettingKeys.notificationCurrentIds) ?? '{}'));
|
jsonDecode(store.getString(SettingKeys.notificationCurrentIds) ?? '{}'),
|
||||||
|
);
|
||||||
int? currentInt;
|
int? currentInt;
|
||||||
try {
|
try {
|
||||||
currentInt = idMap[roomId];
|
currentInt = idMap[roomId];
|
||||||
|
@ -54,12 +54,14 @@ extension RoomStatusExtension on Room {
|
|||||||
}
|
}
|
||||||
} else if (typingUsers.length == 2) {
|
} else if (typingUsers.length == 2) {
|
||||||
typingText = L10n.of(context)!.userAndUserAreTyping(
|
typingText = L10n.of(context)!.userAndUserAreTyping(
|
||||||
typingUsers.first.calcDisplayname(),
|
typingUsers.first.calcDisplayname(),
|
||||||
typingUsers[1].calcDisplayname());
|
typingUsers[1].calcDisplayname(),
|
||||||
|
);
|
||||||
} else if (typingUsers.length > 2) {
|
} else if (typingUsers.length > 2) {
|
||||||
typingText = L10n.of(context)!.userAndOthersAreTyping(
|
typingText = L10n.of(context)!.userAndOthersAreTyping(
|
||||||
typingUsers.first.calcDisplayname(),
|
typingUsers.first.calcDisplayname(),
|
||||||
(typingUsers.length - 1).toString());
|
(typingUsers.length - 1).toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return typingText;
|
return typingText;
|
||||||
}
|
}
|
||||||
@ -76,8 +78,10 @@ extension RoomStatusExtension on Room {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastReceipts.removeWhere((user) =>
|
lastReceipts.removeWhere(
|
||||||
user.id == client.userID || user.id == timeline.events.first.senderId);
|
(user) =>
|
||||||
|
user.id == client.userID || user.id == timeline.events.first.senderId,
|
||||||
|
);
|
||||||
return lastReceipts.toList();
|
return lastReceipts.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,11 @@ extension StreamExtension on Stream {
|
|||||||
gotMessage = true;
|
gotMessage = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
final subscription = listen((_) => onMessage?.call(),
|
final subscription = listen(
|
||||||
onDone: () => controller.close(),
|
(_) => onMessage?.call(),
|
||||||
onError: (e, s) => controller.addError(e, s));
|
onDone: () => controller.close(),
|
||||||
|
onError: (e, s) => controller.addError(e, s),
|
||||||
|
);
|
||||||
// add proper cleanup to the subscription and the controller, to not memory leak
|
// add proper cleanup to the subscription and the controller, to not memory leak
|
||||||
controller.onCancel = () {
|
controller.onCancel = () {
|
||||||
subscription.cancel();
|
subscription.cancel();
|
||||||
|
@ -82,7 +82,8 @@ extension UiaRequestManager on MatrixState {
|
|||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
final url = Uri.parse(
|
final url = Uri.parse(
|
||||||
'${client.homeserver}/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}');
|
'${client.homeserver}/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}',
|
||||||
|
);
|
||||||
launchUrlString(url.toString());
|
launchUrlString(url.toString());
|
||||||
if (OkCancelResult.ok ==
|
if (OkCancelResult.ok ==
|
||||||
await showOkCancelAlertDialog(
|
await showOkCancelAlertDialog(
|
||||||
|
@ -20,7 +20,8 @@ class UpdateCheckerNoStore {
|
|||||||
static const gitLabHost = 'gitlab.com';
|
static const gitLabHost = 'gitlab.com';
|
||||||
|
|
||||||
static Uri get tagsUri => Uri.parse(
|
static Uri get tagsUri => Uri.parse(
|
||||||
'https://$gitLabHost/projects/$gitLabProjectId/repository/tags');
|
'https://$gitLabHost/projects/$gitLabProjectId/repository/tags',
|
||||||
|
);
|
||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
|
@ -33,7 +33,8 @@ class UrlLauncher {
|
|||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
// we can't open this thing
|
// we can't open this thing
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))));
|
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!{'https', 'http'}.contains(uri.scheme)) {
|
if (!{'https', 'http'}.contains(uri.scheme)) {
|
||||||
@ -61,7 +62,8 @@ class UrlLauncher {
|
|||||||
// transmute geo URIs on desktop to openstreetmap links, as those usually can't handle
|
// transmute geo URIs on desktop to openstreetmap links, as those usually can't handle
|
||||||
// geo URIs
|
// geo URIs
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://www.openstreetmap.org/?mlat=${latlong.first}&mlon=${latlong.last}#map=16/${latlong.first}/${latlong.last}');
|
'https://www.openstreetmap.org/?mlat=${latlong.first}&mlon=${latlong.last}#map=16/${latlong.first}/${latlong.last}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -71,7 +73,8 @@ class UrlLauncher {
|
|||||||
}
|
}
|
||||||
if (uri.host.isEmpty) {
|
if (uri.host.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))));
|
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// okay, we have either an http or an https URI.
|
// okay, we have either an http or an https URI.
|
||||||
@ -86,8 +89,10 @@ class UrlLauncher {
|
|||||||
}).join('.');
|
}).join('.');
|
||||||
// Force LaunchMode.externalApplication, otherwise url_launcher will default
|
// Force LaunchMode.externalApplication, otherwise url_launcher will default
|
||||||
// to opening links in a webview on mobile platforms.
|
// to opening links in a webview on mobile platforms.
|
||||||
launchUrlString(uri.replace(host: newHost).toString(),
|
launchUrlString(
|
||||||
mode: LaunchMode.externalApplication);
|
uri.replace(host: newHost).toString(),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openMatrixToUrl() async {
|
void openMatrixToUrl() async {
|
||||||
@ -142,8 +147,10 @@ class UrlLauncher {
|
|||||||
}
|
}
|
||||||
// we have the room, so....just open it
|
// we have the room, so....just open it
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
VRouter.of(context).toSegments(['rooms', room.id],
|
VRouter.of(context).toSegments(
|
||||||
queryParameters: {'event': event});
|
['rooms', room.id],
|
||||||
|
queryParameters: {'event': event},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
VRouter.of(context).toSegments(['rooms', room.id]);
|
VRouter.of(context).toSegments(['rooms', room.id]);
|
||||||
}
|
}
|
||||||
@ -175,11 +182,14 @@ class UrlLauncher {
|
|||||||
if (response.error != null) return;
|
if (response.error != null) return;
|
||||||
// wait for two seconds so that it probably came down /sync
|
// wait for two seconds so that it probably came down /sync
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => Future.delayed(const Duration(seconds: 2)));
|
future: () => Future.delayed(const Duration(seconds: 2)),
|
||||||
|
);
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
VRouter.of(context).toSegments(['rooms', response.result!],
|
VRouter.of(context).toSegments(
|
||||||
queryParameters: {'event': event});
|
['rooms', response.result!],
|
||||||
|
queryParameters: {'event': event},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
VRouter.of(context).toSegments(['rooms', response.result!]);
|
VRouter.of(context).toSegments(['rooms', response.result!]);
|
||||||
}
|
}
|
||||||
|
@ -111,14 +111,15 @@ class CallKeepManager {
|
|||||||
Future<void> showCallkitIncoming(CallSession call) async {
|
Future<void> showCallkitIncoming(CallSession call) async {
|
||||||
if (!setupDone) {
|
if (!setupDone) {
|
||||||
await _callKeep.setup(
|
await _callKeep.setup(
|
||||||
null,
|
null,
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'ios': <String, dynamic>{
|
'ios': <String, dynamic>{
|
||||||
'appName': appName,
|
'appName': appName,
|
||||||
},
|
|
||||||
'android': alertOptions,
|
|
||||||
},
|
},
|
||||||
backgroundMode: true);
|
'android': alertOptions,
|
||||||
|
},
|
||||||
|
backgroundMode: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setupDone = true;
|
setupDone = true;
|
||||||
await displayIncomingCall(call);
|
await displayIncomingCall(call);
|
||||||
@ -131,7 +132,8 @@ class CallKeepManager {
|
|||||||
(event) {
|
(event) {
|
||||||
if (event == CallEvent.kLocalHoldUnhold) {
|
if (event == CallEvent.kLocalHoldUnhold) {
|
||||||
Logs().i(
|
Logs().i(
|
||||||
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}');
|
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -169,10 +171,14 @@ class CallKeepManager {
|
|||||||
_callKeep.on(CallKeepPerformAnswerCallAction(), answerCall);
|
_callKeep.on(CallKeepPerformAnswerCallAction(), answerCall);
|
||||||
_callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction);
|
_callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction);
|
||||||
_callKeep.on(
|
_callKeep.on(
|
||||||
CallKeepDidReceiveStartCallAction(), didReceiveStartCallAction);
|
CallKeepDidReceiveStartCallAction(),
|
||||||
|
didReceiveStartCallAction,
|
||||||
|
);
|
||||||
_callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction);
|
_callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction);
|
||||||
_callKeep.on(
|
_callKeep.on(
|
||||||
CallKeepDidPerformSetMutedCallAction(), didPerformSetMutedCallAction);
|
CallKeepDidPerformSetMutedCallAction(),
|
||||||
|
didPerformSetMutedCallAction,
|
||||||
|
);
|
||||||
_callKeep.on(CallKeepPerformEndCallAction(), endCall);
|
_callKeep.on(CallKeepPerformEndCallAction(), endCall);
|
||||||
_callKeep.on(CallKeepPushKitToken(), onPushKitToken);
|
_callKeep.on(CallKeepPushKitToken(), onPushKitToken);
|
||||||
_callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall);
|
_callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall);
|
||||||
@ -209,11 +215,17 @@ class CallKeepManager {
|
|||||||
Future<void> updateDisplay(String callUUID) async {
|
Future<void> updateDisplay(String callUUID) async {
|
||||||
// Workaround because Android doesn't display well displayName, se we have to switch ...
|
// Workaround because Android doesn't display well displayName, se we have to switch ...
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
await _callKeep.updateDisplay(callUUID,
|
await _callKeep.updateDisplay(
|
||||||
displayName: 'New Name', handle: callUUID);
|
callUUID,
|
||||||
|
displayName: 'New Name',
|
||||||
|
handle: callUUID,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await _callKeep.updateDisplay(callUUID,
|
await _callKeep.updateDisplay(
|
||||||
displayName: callUUID, handle: 'New Name');
|
callUUID,
|
||||||
|
displayName: callUUID,
|
||||||
|
handle: 'New Name',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +262,8 @@ class CallKeepManager {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => FlutterForegroundTask.openSystemAlertWindowSettings(
|
onTap: () => FlutterForegroundTask.openSystemAlertWindowSettings(
|
||||||
forceOpen: true),
|
forceOpen: true,
|
||||||
|
),
|
||||||
title: Text(L10n.of(context)!.appearOnTop),
|
title: Text(L10n.of(context)!.appearOnTop),
|
||||||
subtitle: Text(L10n.of(context)!.appearOnTopDetails),
|
subtitle: Text(L10n.of(context)!.appearOnTopDetails),
|
||||||
trailing: const Icon(Icons.file_upload_rounded),
|
trailing: const Icon(Icons.file_upload_rounded),
|
||||||
@ -310,7 +323,8 @@ class CallKeepManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> didReceiveStartCallAction(
|
Future<void> didReceiveStartCallAction(
|
||||||
CallKeepDidReceiveStartCallAction event) async {
|
CallKeepDidReceiveStartCallAction event,
|
||||||
|
) async {
|
||||||
if (event.handle == null) {
|
if (event.handle == null) {
|
||||||
// @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined`
|
// @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined`
|
||||||
return;
|
return;
|
||||||
@ -328,7 +342,8 @@ class CallKeepManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> didPerformSetMutedCallAction(
|
Future<void> didPerformSetMutedCallAction(
|
||||||
CallKeepDidPerformSetMutedCallAction event) async {
|
CallKeepDidPerformSetMutedCallAction event,
|
||||||
|
) async {
|
||||||
final keeper = calls[event.callUUID];
|
final keeper = calls[event.callUUID];
|
||||||
if (event.muted!) {
|
if (event.muted!) {
|
||||||
keeper!.call.setMicrophoneMuted(true);
|
keeper!.call.setMicrophoneMuted(true);
|
||||||
@ -339,7 +354,8 @@ class CallKeepManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> didToggleHoldCallAction(
|
Future<void> didToggleHoldCallAction(
|
||||||
CallKeepDidToggleHoldAction event) async {
|
CallKeepDidToggleHoldAction event,
|
||||||
|
) async {
|
||||||
final keeper = calls[event.callUUID];
|
final keeper = calls[event.callUUID];
|
||||||
if (event.hold!) {
|
if (event.hold!) {
|
||||||
keeper!.call.setRemoteOnHold(true);
|
keeper!.call.setRemoteOnHold(true);
|
||||||
|
@ -82,14 +82,15 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
|||||||
} else {
|
} else {
|
||||||
overlayEntry = OverlayEntry(
|
overlayEntry = OverlayEntry(
|
||||||
builder: (_) => Calling(
|
builder: (_) => Calling(
|
||||||
context: context,
|
context: context,
|
||||||
client: client,
|
client: client,
|
||||||
callId: callId,
|
callId: callId,
|
||||||
call: call,
|
call: call,
|
||||||
onClear: () {
|
onClear: () {
|
||||||
overlayEntry?.remove();
|
overlayEntry?.remove();
|
||||||
overlayEntry = null;
|
overlayEntry = null;
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
Overlay.of(context).insert(overlayEntry!);
|
Overlay.of(context).insert(overlayEntry!);
|
||||||
}
|
}
|
||||||
@ -103,8 +104,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<RTCPeerConnection> createPeerConnection(
|
Future<RTCPeerConnection> createPeerConnection(
|
||||||
Map<String, dynamic> configuration,
|
Map<String, dynamic> configuration, [
|
||||||
[Map<String, dynamic> constraints = const {}]) =>
|
Map<String, dynamic> constraints = const {},
|
||||||
|
]) =>
|
||||||
webrtc_impl.createPeerConnection(configuration, constraints);
|
webrtc_impl.createPeerConnection(configuration, constraints);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -150,7 +152,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
|||||||
try {
|
try {
|
||||||
final wasForeground = await FlutterForegroundTask.isAppOnForeground;
|
final wasForeground = await FlutterForegroundTask.isAppOnForeground;
|
||||||
await Store().setItem(
|
await Store().setItem(
|
||||||
'wasForeground', wasForeground == true ? 'true' : 'false');
|
'wasForeground',
|
||||||
|
wasForeground == true ? 'true' : 'false',
|
||||||
|
);
|
||||||
FlutterForegroundTask.setOnLockScreenVisibility(true);
|
FlutterForegroundTask.setOnLockScreenVisibility(true);
|
||||||
FlutterForegroundTask.wakeUpScreen();
|
FlutterForegroundTask.wakeUpScreen();
|
||||||
FlutterForegroundTask.launchApp();
|
FlutterForegroundTask.launchApp();
|
||||||
@ -162,10 +166,13 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
|||||||
try {
|
try {
|
||||||
if (!hasCallingAccount) {
|
if (!hasCallingAccount) {
|
||||||
ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!)
|
ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!)
|
||||||
.showSnackBar(const SnackBar(
|
.showSnackBar(
|
||||||
content: Text(
|
const SnackBar(
|
||||||
'No calling accounts found (used for native calls UI)',
|
content: Text(
|
||||||
)));
|
'No calling accounts found (used for native calls UI)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logs().e('failed to show snackbar');
|
Logs().e('failed to show snackbar');
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user