mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-19 10:39:26 +01:00
Merge branch 'malin/addRequireTrailingCommasRule' into 'main'
refactor: Added and applied require_trailing_commas linter rule See merge request famedly/fluffychat!1091
This commit is contained in:
commit
e461cb1f53
@ -8,6 +8,7 @@ linter:
|
||||
- prefer_final_locals
|
||||
- prefer_final_in_for_each
|
||||
- sort_pub_dependencies
|
||||
- require_trailing_commas
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
|
@ -144,13 +144,15 @@ void main() {
|
||||
|
||||
await tester.waitFor(
|
||||
find.descendant(
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(TextField)),
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
);
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(TextField)),
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
Users.user2.name,
|
||||
);
|
||||
|
||||
@ -160,14 +162,17 @@ void main() {
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find
|
||||
.descendant(
|
||||
await tester.tap(
|
||||
find
|
||||
.descendant(
|
||||
of: find.descendant(
|
||||
of: find.byType(InvitationSelectionView),
|
||||
matching: find.byType(ListTile),
|
||||
),
|
||||
matching: find.text(Users.user2.name))
|
||||
.last);
|
||||
matching: find.text(Users.user2.name),
|
||||
)
|
||||
.last,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.waitFor(find.maybeUppercaseText('Yes'));
|
||||
|
@ -144,7 +144,8 @@ extension DefaultFlowExtensions on WidgetTester {
|
||||
do {
|
||||
if (DateTime.now().isAfter(end)) {
|
||||
throw Exception(
|
||||
'Timed out waiting for HomeserverPicker or ChatListViewBody');
|
||||
'Timed out waiting for HomeserverPicker or ChatListViewBody',
|
||||
);
|
||||
}
|
||||
|
||||
await pumpAndSettle();
|
||||
|
@ -70,8 +70,9 @@ abstract class AppConfig {
|
||||
colorSchemeSeed = Color(json['chat_color']);
|
||||
} catch (e) {
|
||||
Logs().w(
|
||||
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
|
||||
e);
|
||||
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (json['application_name'] is String) {
|
||||
|
@ -70,21 +70,25 @@ class AppRoutes {
|
||||
widget: const ChatDetails(),
|
||||
stackedRoutes: _chatDetailsRoutes,
|
||||
),
|
||||
VWidget(path: ':roomid', widget: const Chat(), stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'encryption',
|
||||
widget: const ChatEncryptionSettings(),
|
||||
),
|
||||
VWidget(
|
||||
path: 'invite',
|
||||
widget: const InvitationSelection(),
|
||||
),
|
||||
VWidget(
|
||||
path: 'details',
|
||||
widget: const ChatDetails(),
|
||||
stackedRoutes: _chatDetailsRoutes,
|
||||
),
|
||||
]),
|
||||
VWidget(
|
||||
path: ':roomid',
|
||||
widget: const Chat(),
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'encryption',
|
||||
widget: const ChatEncryptionSettings(),
|
||||
),
|
||||
VWidget(
|
||||
path: 'invite',
|
||||
widget: const InvitationSelection(),
|
||||
),
|
||||
VWidget(
|
||||
path: 'details',
|
||||
widget: const ChatDetails(),
|
||||
stackedRoutes: _chatDetailsRoutes,
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
path: '/settings',
|
||||
widget: const Settings(),
|
||||
@ -263,21 +267,22 @@ class AppRoutes {
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'connect',
|
||||
widget: const ConnectPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'login',
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'signup',
|
||||
widget: const SignupPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
]),
|
||||
path: 'connect',
|
||||
widget: const ConnectPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'login',
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'signup',
|
||||
widget: const SignupPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
path: 'logs',
|
||||
widget: const LogViewer(),
|
||||
@ -354,21 +359,22 @@ class AppRoutes {
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'connect',
|
||||
widget: const ConnectPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'login',
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'signup',
|
||||
widget: const SignupPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
]),
|
||||
path: 'connect',
|
||||
widget: const ConnectPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: 'login',
|
||||
widget: const Login(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: 'signup',
|
||||
widget: const SignupPage(),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
|
@ -88,14 +88,15 @@ class AddStoryController extends State<AddStoryPage> {
|
||||
);
|
||||
if (picked == null) return;
|
||||
final matrixFile = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final bytes = await picked.readAsBytes();
|
||||
return MatrixImageFile(
|
||||
bytes: bytes,
|
||||
name: picked.name,
|
||||
);
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
final bytes = await picked.readAsBytes();
|
||||
return MatrixImageFile(
|
||||
bytes: bytes,
|
||||
name: picked.name,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
setState(() {
|
||||
image = matrixFile.result;
|
||||
|
@ -92,34 +92,39 @@ class InviteStoryPageState extends State<InviteStoryPage> {
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<User>>(
|
||||
future: loadContacts,
|
||||
builder: (context, snapshot) {
|
||||
final contacts = snapshot.data;
|
||||
if (contacts == null) {
|
||||
final error = snapshot.error;
|
||||
if (error != null) {
|
||||
return Center(
|
||||
child: Text(error.toLocalizedString(context)));
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive());
|
||||
future: loadContacts,
|
||||
builder: (context, snapshot) {
|
||||
final contacts = snapshot.data;
|
||||
if (contacts == null) {
|
||||
final error = snapshot.error;
|
||||
if (error != null) {
|
||||
return Center(
|
||||
child: Text(error.toLocalizedString(context)),
|
||||
);
|
||||
}
|
||||
_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()),
|
||||
),
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}),
|
||||
}
|
||||
_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) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
child: Text(
|
||||
L10n.of(context)!.oopsSomethingWentWrong,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
);
|
||||
} else {
|
||||
archive = snapshot.data;
|
||||
if (archive == null || archive!.isEmpty) {
|
||||
return const Center(
|
||||
child: Icon(Icons.archive_outlined, size: 80));
|
||||
child: Icon(Icons.archive_outlined, size: 80),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: archive!.length,
|
||||
|
@ -246,7 +246,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 1.5),
|
||||
maxWidth: FluffyThemes.columnWidth * 1.5,
|
||||
),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
@ -258,7 +259,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.pleaseEnterRecoveryKeyDescription),
|
||||
L10n.of(context)!.pleaseEnterRecoveryKeyDescription,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
TextField(
|
||||
@ -274,64 +276,68 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.fontFamily),
|
||||
fontFamily:
|
||||
Theme.of(context).textTheme.bodyLarge?.fontFamily,
|
||||
),
|
||||
hintText: L10n.of(context)!.recoveryKey,
|
||||
errorText: _recoveryKeyInputError,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
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),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
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),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.cast_connected_outlined),
|
||||
@ -408,11 +414,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
case BootstrapState.error:
|
||||
titleText = L10n.of(context)!.oopsSomethingWentWrong;
|
||||
body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context)!.close,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||
));
|
||||
buttons.add(
|
||||
AdaptiveFlatButton(
|
||||
label: L10n.of(context)!.close,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BootstrapState.done:
|
||||
titleText = L10n.of(context)!.everythingReady;
|
||||
@ -423,11 +431,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
|
||||
],
|
||||
);
|
||||
buttons.add(AdaptiveFlatButton(
|
||||
label: L10n.of(context)!.close,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||
));
|
||||
buttons.add(
|
||||
AdaptiveFlatButton(
|
||||
label: L10n.of(context)!.close,
|
||||
onPressed: () =>
|
||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,8 @@ class AddWidgetTileState extends State<AddWidgetTile> {
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
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.video': Text(L10n.of(context)!.widgetVideo),
|
||||
'm.custom': Text(L10n.of(context)!.widgetCustom),
|
||||
}.map((key, value) => MapEntry(
|
||||
}.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: value,
|
||||
))),
|
||||
),
|
||||
),
|
||||
),
|
||||
onValueChanged: controller.setWidgetType,
|
||||
),
|
||||
Padding(
|
||||
|
@ -82,10 +82,12 @@ class ChatController extends State<Chat> {
|
||||
|
||||
final matrixFiles = <MatrixFile>[];
|
||||
for (var i = 0; i < bytesList.result!.length; i++) {
|
||||
matrixFiles.add(MatrixFile(
|
||||
bytes: bytesList.result![i],
|
||||
name: details.files[i].name,
|
||||
).detectFileType);
|
||||
matrixFiles.add(
|
||||
MatrixFile(
|
||||
bytes: bytesList.result![i],
|
||||
name: details.files[i].name,
|
||||
).detectFileType,
|
||||
);
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
@ -139,18 +141,20 @@ class ChatController extends State<Chat> {
|
||||
final userId = room?.directChatMatrixID;
|
||||
if (room == null || userId == null) {
|
||||
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(
|
||||
context: context,
|
||||
future: () async {
|
||||
final client = room.client;
|
||||
final waitForSync = client.onSync.stream
|
||||
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
|
||||
await room.leave();
|
||||
await waitForSync;
|
||||
return await client.startDirectChat(userId);
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
final client = room.client;
|
||||
final waitForSync = client.onSync.stream
|
||||
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
|
||||
await room.leave();
|
||||
await waitForSync;
|
||||
return await client.startDirectChat(userId);
|
||||
},
|
||||
);
|
||||
final roomId = success.result;
|
||||
if (roomId == null) return;
|
||||
VRouter.of(context).toSegments(['rooms', roomId]);
|
||||
@ -160,7 +164,8 @@ class ChatController extends State<Chat> {
|
||||
final room = this.room;
|
||||
if (room == null) {
|
||||
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(
|
||||
context: context,
|
||||
@ -327,10 +332,12 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
|
||||
// ignore: unawaited_futures
|
||||
room!.sendTextEvent(sendController.text,
|
||||
inReplyTo: replyEvent,
|
||||
editEventId: editEvent?.eventId,
|
||||
parseCommands: parseCommands);
|
||||
room!.sendTextEvent(
|
||||
sendController.text,
|
||||
inReplyTo: replyEvent,
|
||||
editEventId: editEvent?.eventId,
|
||||
parseCommands: parseCommands,
|
||||
);
|
||||
sendController.value = TextEditingValue(
|
||||
text: pendingText,
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
@ -354,10 +361,12 @@ class ChatController extends State<Chat> {
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
files: result
|
||||
.map((xfile) => MatrixFile(
|
||||
bytes: xfile.toUint8List(),
|
||||
name: xfile.fileName!,
|
||||
).detectFileType)
|
||||
.map(
|
||||
(xfile) => MatrixFile(
|
||||
bytes: xfile.toUint8List(),
|
||||
name: xfile.fileName!,
|
||||
).detectFileType,
|
||||
)
|
||||
.toList(),
|
||||
room: room!,
|
||||
),
|
||||
@ -375,10 +384,12 @@ class ChatController extends State<Chat> {
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
files: result
|
||||
.map((xfile) => MatrixFile(
|
||||
bytes: xfile.toUint8List(),
|
||||
name: xfile.fileName!,
|
||||
).detectFileType)
|
||||
.map(
|
||||
(xfile) => MatrixFile(
|
||||
bytes: xfile.toUint8List(),
|
||||
name: xfile.fileName!,
|
||||
).detectFileType,
|
||||
)
|
||||
.toList(),
|
||||
room: room!,
|
||||
),
|
||||
@ -537,8 +548,9 @@ class ChatController extends State<Chat> {
|
||||
for (final event in selectedEvents) {
|
||||
if (copyString.isNotEmpty) copyString += '\n\n';
|
||||
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true);
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
);
|
||||
}
|
||||
return copyString;
|
||||
}
|
||||
@ -554,33 +566,35 @@ class ChatController extends State<Chat> {
|
||||
void reportEventAction() async {
|
||||
final event = selectedEvents.single;
|
||||
final score = await showConfirmationDialog<int>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.reportMessage,
|
||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context)!.extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context)!.offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context)!.inoffensive,
|
||||
),
|
||||
]);
|
||||
context: context,
|
||||
title: L10n.of(context)!.reportMessage,
|
||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context)!.extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context)!.offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context)!.inoffensive,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (score == null) return;
|
||||
final reason = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||
);
|
||||
if (reason == null || reason.single.isEmpty) return;
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
@ -597,7 +611,8 @@ class ChatController extends State<Chat> {
|
||||
selectedEvents.clear();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)));
|
||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
|
||||
);
|
||||
}
|
||||
|
||||
void redactEventsAction() async {
|
||||
@ -612,25 +627,27 @@ class ChatController extends State<Chat> {
|
||||
if (!confirmed) return;
|
||||
for (final event in selectedEvents) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
if (event.status.isSent) {
|
||||
if (event.canRedact) {
|
||||
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();
|
||||
}
|
||||
context: context,
|
||||
future: () async {
|
||||
if (event.status.isSent) {
|
||||
if (event.canRedact) {
|
||||
await event.redactEvent();
|
||||
} 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(() {
|
||||
showEmojiPicker = false;
|
||||
@ -706,41 +723,42 @@ class ChatController extends State<Chat> {
|
||||
// event id not found...maybe we can fetch it?
|
||||
// the try...finally is here to start and close the loading dialog reliably
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
// okay, we first have to fetch if the event is in the room
|
||||
context: context,
|
||||
future: () async {
|
||||
// okay, we first have to fetch if the event is in the room
|
||||
try {
|
||||
final event = await timeline!.getEventById(eventId);
|
||||
if (event == null) {
|
||||
// event is null...meaning something is off
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||
// event wasn't found, as the server gave a 404 or something
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
// okay, we know that the event *is* in the room
|
||||
while (eventIndex == -1) {
|
||||
if (!canLoadMore) {
|
||||
// we can't load any more events but still haven't found ours yet...better stop here
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final event = await timeline!.getEventById(eventId);
|
||||
if (event == null) {
|
||||
// event is null...meaning something is off
|
||||
return;
|
||||
}
|
||||
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
||||
} catch (err) {
|
||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||
// event wasn't found, as the server gave a 404 or something
|
||||
if (err is TimeoutException) {
|
||||
// loading the history timed out...so let's do nothing
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
// okay, we know that the event *is* in the room
|
||||
while (eventIndex == -1) {
|
||||
if (!canLoadMore) {
|
||||
// we can't load any more events but still haven't found ours yet...better stop here
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
||||
} catch (err) {
|
||||
if (err is TimeoutException) {
|
||||
// loading the history timed out...so let's do nothing
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
eventIndex =
|
||||
timeline!.events.indexWhere((e) => e.eventId == eventId);
|
||||
}
|
||||
});
|
||||
eventIndex =
|
||||
timeline!.events.indexWhere((e) => e.eventId == eventId);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -811,7 +829,8 @@ class ChatController extends State<Chat> {
|
||||
sendController
|
||||
..text = sendController.text.characters.skipLast(1).toString()
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: sendController.text.length));
|
||||
TextPosition(offset: sendController.text.length),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -846,8 +865,9 @@ class ChatController extends State<Chat> {
|
||||
|
||||
void editSelectedEventAction() {
|
||||
final client = currentRoomBundle.firstWhere(
|
||||
(cl) => selectedEvents.first.senderId == cl!.userID,
|
||||
orElse: () => null);
|
||||
(cl) => selectedEvents.first.senderId == cl!.userID,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
@ -855,10 +875,12 @@ class ChatController extends State<Chat> {
|
||||
setState(() {
|
||||
pendingText = sendController.text;
|
||||
editEvent = selectedEvents.first;
|
||||
inputText = sendController.text = editEvent!
|
||||
.getDisplayEvent(timeline!)
|
||||
.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false, hideReply: true);
|
||||
inputText = sendController.text =
|
||||
editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
);
|
||||
selectedEvents.clear();
|
||||
});
|
||||
inputFocus.requestFocus();
|
||||
@ -881,10 +903,12 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room!.client.joinRoom(room!
|
||||
.getState(EventTypes.RoomTombstone)!
|
||||
.parsedTombstoneContent
|
||||
.replacementRoom),
|
||||
future: () => room!.client.joinRoom(
|
||||
room!
|
||||
.getState(EventTypes.RoomTombstone)!
|
||||
.parsedTombstoneContent
|
||||
.replacementRoom,
|
||||
),
|
||||
);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
@ -1077,9 +1101,10 @@ class ChatController extends State<Chat> {
|
||||
if (callType == null) return;
|
||||
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials());
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials(),
|
||||
);
|
||||
if (success.result != null) {
|
||||
final voipPlugin = Matrix.of(context).voipPlugin;
|
||||
try {
|
||||
|
@ -86,19 +86,20 @@ class ChatEventList extends StatelessWidget {
|
||||
index: i - 1,
|
||||
controller: controller.scrollController,
|
||||
child: event.isVisibleInGui
|
||||
? Message(event,
|
||||
? Message(
|
||||
event,
|
||||
onSwipe: (direction) =>
|
||||
controller.replyAction(replyTo: event),
|
||||
onInfoTab: controller.showEventInfo,
|
||||
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: event.senderFromMemoryOrFallback,
|
||||
outerContext: context,
|
||||
onMention: () => controller.sendController.text +=
|
||||
'${event.senderFromMemoryOrFallback.mention} ',
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: event.senderFromMemoryOrFallback,
|
||||
outerContext: context,
|
||||
onMention: () => controller.sendController.text +=
|
||||
'${event.senderFromMemoryOrFallback.mention} ',
|
||||
),
|
||||
),
|
||||
onSelect: controller.onSelectMessage,
|
||||
scrollToEventId: (String eventId) =>
|
||||
controller.scrollToEventId(eventId),
|
||||
@ -108,7 +109,8 @@ class ChatEventList extends StatelessWidget {
|
||||
timeline: controller.timeline!,
|
||||
nextEvent: i < controller.timeline!.events.length
|
||||
? controller.timeline!.events[i]
|
||||
: null)
|
||||
: null,
|
||||
)
|
||||
: Container(),
|
||||
);
|
||||
},
|
||||
|
@ -300,23 +300,24 @@ class _ChatAccountPicker extends StatelessWidget {
|
||||
builder: (context, snapshot) => PopupMenuButton<String>(
|
||||
onSelected: _popupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) => clients
|
||||
.map((client) => PopupMenuItem<String>(
|
||||
value: client!.userID,
|
||||
child: FutureBuilder<Profile>(
|
||||
future: client.fetchOwnProfile(),
|
||||
builder: (context, snapshot) => ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name: snapshot.data?.displayName ??
|
||||
client.userID!.localpart,
|
||||
size: 20,
|
||||
),
|
||||
title:
|
||||
Text(snapshot.data?.displayName ?? client.userID!),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
.map(
|
||||
(client) => PopupMenuItem<String>(
|
||||
value: client!.userID,
|
||||
child: FutureBuilder<Profile>(
|
||||
future: client.fetchOwnProfile(),
|
||||
builder: (context, snapshot) => ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name: snapshot.data?.displayName ??
|
||||
client.userID!.localpart,
|
||||
size: 20,
|
||||
),
|
||||
title: Text(snapshot.data?.displayName ?? client.userID!),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
))
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
|
@ -49,11 +49,12 @@ class ChatView extends StatelessWidget {
|
||||
if (controller.canSaveSelectedEvent)
|
||||
// Use builder context to correctly position the share dialog on iPad
|
||||
Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: Icon(Icons.adaptive.share),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
onPressed: () => controller.saveSelectedEvent(context),
|
||||
)),
|
||||
builder: (context) => IconButton(
|
||||
icon: Icon(Icons.adaptive.share),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
onPressed: () => controller.saveSelectedEvent(context),
|
||||
),
|
||||
),
|
||||
if (controller.canRedactSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
@ -155,7 +156,9 @@ class ChatView extends StatelessWidget {
|
||||
|
||||
if (controller.room!.membership == Membership.invite) {
|
||||
showFutureLoadingDialog(
|
||||
context: context, future: () => controller.room!.join());
|
||||
context: context,
|
||||
future: () => controller.room!.join(),
|
||||
);
|
||||
}
|
||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@ -249,21 +252,23 @@ class ChatView extends StatelessWidget {
|
||||
PinnedEvents(controller),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator
|
||||
.adaptive(strokeWidth: 2),
|
||||
);
|
||||
}
|
||||
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,),
|
||||
);
|
||||
},
|
||||
)),
|
||||
}
|
||||
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.room!.canSendDefaultMessages &&
|
||||
controller.room!.membership == Membership.join)
|
||||
@ -274,7 +279,8 @@ class ChatView extends StatelessWidget {
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5),
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.only(
|
||||
@ -324,7 +330,8 @@ class ChatView extends StatelessWidget {
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.reopenChat),
|
||||
L10n.of(context)!.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
@ -15,15 +15,18 @@ class EditWidgetsDialog extends StatelessWidget {
|
||||
return SimpleDialog(
|
||||
title: Text(L10n.of(context)!.editWidgets),
|
||||
children: [
|
||||
...room.widgets.map((e) => ListTile(
|
||||
title: Text(e.name ?? e.type),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
room.deleteWidget(e.id!);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.delete)),
|
||||
)),
|
||||
...room.widgets.map(
|
||||
(e) => ListTile(
|
||||
title: Text(e.name ?? e.type),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
room.deleteWidget(e.id!);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
),
|
||||
),
|
||||
AddWidgetTile(room: room),
|
||||
],
|
||||
);
|
||||
|
@ -13,34 +13,34 @@ class EncryptionButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<SyncUpdate>(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.deviceLists != null),
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<EncryptionHealthState>(
|
||||
future: room.calcEncryptionHealthState(),
|
||||
builder: (BuildContext context, snapshot) => IconButton(
|
||||
tooltip: room.encrypted
|
||||
? L10n.of(context)!.encrypted
|
||||
: L10n.of(context)!.encryptionNotEnabled,
|
||||
icon: Icon(
|
||||
room.encrypted
|
||||
? Icons.lock_outlined
|
||||
: Icons.lock_open_outlined,
|
||||
size: 20,
|
||||
color: room.joinRules != JoinRules.public &&
|
||||
!room.encrypted
|
||||
? Colors.red
|
||||
: room.joinRules != JoinRules.public &&
|
||||
snapshot.data ==
|
||||
EncryptionHealthState.unverifiedDevices
|
||||
? Colors.orange
|
||||
: null),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', room.id, 'encryption']),
|
||||
));
|
||||
});
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.deviceLists != null),
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<EncryptionHealthState>(
|
||||
future: room.calcEncryptionHealthState(),
|
||||
builder: (BuildContext context, snapshot) => IconButton(
|
||||
tooltip: room.encrypted
|
||||
? L10n.of(context)!.encrypted
|
||||
: L10n.of(context)!.encryptionNotEnabled,
|
||||
icon: Icon(
|
||||
room.encrypted ? Icons.lock_outlined : Icons.lock_open_outlined,
|
||||
size: 20,
|
||||
color: room.joinRules != JoinRules.public && !room.encrypted
|
||||
? Colors.red
|
||||
: room.joinRules != JoinRules.public &&
|
||||
snapshot.data ==
|
||||
EncryptionHealthState.unverifiedDevices
|
||||
? Colors.orange
|
||||
: null,
|
||||
),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', room.id, 'encryption']),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ class EventInfoDialog extends StatelessWidget {
|
||||
),
|
||||
title: Text(L10n.of(context)!.sender),
|
||||
subtitle: Text(
|
||||
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]'),
|
||||
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.time),
|
||||
|
@ -69,7 +69,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
||||
if (!kIsWeb) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = Uri.encodeComponent(
|
||||
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last);
|
||||
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||
);
|
||||
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
|
||||
await file.writeAsBytes(matrixFile.bytes);
|
||||
}
|
||||
@ -224,23 +225,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
||||
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => audioPlayer?.seek(Duration(
|
||||
onTap: () => audioPlayer?.seek(
|
||||
Duration(
|
||||
milliseconds:
|
||||
(maxPosition / AudioPlayerWidget.wavesCount)
|
||||
.round() *
|
||||
i)),
|
||||
i,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
height: 32,
|
||||
alignment: Alignment.center,
|
||||
child: Opacity(
|
||||
opacity: currentPosition > i ? 1 : 0.5,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
),
|
||||
height: 32 * (waveform[i] / 1024)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
),
|
||||
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,
|
||||
// strip it.
|
||||
final renderHtml = html.replaceAll(
|
||||
RegExp('<mx-reply>.*</mx-reply>',
|
||||
caseSensitive: false, multiLine: false, dotAll: true),
|
||||
'');
|
||||
RegExp(
|
||||
'<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
|
||||
|
||||
@ -61,8 +66,12 @@ class HtmlMessage extends StatelessWidget {
|
||||
maxLines: maxLines,
|
||||
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
getMxcUrl: (String mxc, double? width, double? height,
|
||||
{bool? animated = false}) {
|
||||
getMxcUrl: (
|
||||
String mxc,
|
||||
double? width,
|
||||
double? height, {
|
||||
bool? animated = false,
|
||||
}) {
|
||||
final ratio = MediaQuery.of(context).devicePixelRatio;
|
||||
return Uri.parse(mxc)
|
||||
.getThumbnail(
|
||||
@ -75,19 +84,23 @@ class HtmlMessage extends StatelessWidget {
|
||||
.toString();
|
||||
},
|
||||
onImageTap: (String mxc) => showDialog(
|
||||
context: Matrix.of(context).navigatorContext,
|
||||
useRootNavigator: false,
|
||||
builder: (_) => ImageViewer(Event(
|
||||
type: EventTypes.Message,
|
||||
content: <String, dynamic>{
|
||||
'body': mxc,
|
||||
'url': mxc,
|
||||
'msgtype': MessageTypes.Image,
|
||||
},
|
||||
senderId: room.client.userID!,
|
||||
originServerTs: DateTime.now(),
|
||||
eventId: 'fake_event',
|
||||
room: room))),
|
||||
context: Matrix.of(context).navigatorContext,
|
||||
useRootNavigator: false,
|
||||
builder: (_) => ImageViewer(
|
||||
Event(
|
||||
type: EventTypes.Message,
|
||||
content: <String, dynamic>{
|
||||
'body': mxc,
|
||||
'url': mxc,
|
||||
'msgtype': MessageTypes.Image,
|
||||
},
|
||||
senderId: room.client.userID!,
|
||||
originServerTs: DateTime.now(),
|
||||
eventId: 'fake_event',
|
||||
room: room,
|
||||
),
|
||||
),
|
||||
),
|
||||
setCodeLanguage: (String key, String value) async {
|
||||
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
|
||||
},
|
||||
|
@ -27,18 +27,19 @@ class Message extends StatelessWidget {
|
||||
final bool selected;
|
||||
final Timeline timeline;
|
||||
|
||||
const Message(this.event,
|
||||
{this.nextEvent,
|
||||
this.longPressSelect = false,
|
||||
this.onSelect,
|
||||
this.onInfoTab,
|
||||
this.onAvatarTab,
|
||||
this.scrollToEventId,
|
||||
required this.onSwipe,
|
||||
this.selected = false,
|
||||
required this.timeline,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
const Message(
|
||||
this.event, {
|
||||
this.nextEvent,
|
||||
this.longPressSelect = false,
|
||||
this.onSelect,
|
||||
this.onInfoTab,
|
||||
this.onAvatarTab,
|
||||
this.scrollToEventId,
|
||||
required this.onSwipe,
|
||||
this.selected = false,
|
||||
required this.timeline,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Indicates wheither the user may use a mouse instead
|
||||
/// of touchscreen.
|
||||
@ -126,13 +127,15 @@ class Message extends StatelessWidget {
|
||||
height: 16 * AppConfig.bubbleSizeFactor,
|
||||
child: event.status == EventStatus.sending
|
||||
? const CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2)
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: event.status == EventStatus.error
|
||||
? const Icon(Icons.error, color: Colors.red)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
))
|
||||
),
|
||||
)
|
||||
: FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
@ -142,7 +145,8 @@ class Message extends StatelessWidget {
|
||||
name: user.calcDisplayname(),
|
||||
onTap: () => onAvatarTab!(event),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -171,7 +175,8 @@ class Message extends StatelessWidget {
|
||||
: displayname.lightColorText),
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
alignment: alignment,
|
||||
@ -198,7 +203,8 @@ class Message extends StatelessWidget {
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 1.5),
|
||||
maxWidth: FluffyThemes.columnWidth * 1.5,
|
||||
),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
@ -233,11 +239,14 @@ class Message extends StatelessWidget {
|
||||
child: AbsorbPointer(
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: 4.0 *
|
||||
AppConfig.bubbleSizeFactor),
|
||||
child: ReplyContent(replyEvent,
|
||||
ownMessage: ownMessage,
|
||||
timeline: timeline),
|
||||
vertical:
|
||||
4.0 * AppConfig.bubbleSizeFactor,
|
||||
),
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage: ownMessage,
|
||||
timeline: timeline,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -249,10 +258,13 @@ class Message extends StatelessWidget {
|
||||
onInfoTab: onInfoTab,
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline, RelationshipTypes.edit))
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
))
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4.0 * AppConfig.bubbleSizeFactor),
|
||||
top: 4.0 * AppConfig.bubbleSizeFactor,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -301,26 +313,29 @@ class Message extends StatelessWidget {
|
||||
Padding(
|
||||
padding: displayTime
|
||||
? EdgeInsets.symmetric(
|
||||
vertical: 8.0 * AppConfig.bubbleSizeFactor)
|
||||
vertical: 8.0 * AppConfig.bubbleSizeFactor,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: displayTime
|
||||
? Theme.of(context).colorScheme.background
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.33),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
event.originServerTs.localizedTime(context),
|
||||
style: TextStyle(fontSize: 14 * AppConfig.fontSizeFactor),
|
||||
child: Material(
|
||||
color: displayTime
|
||||
? Theme.of(context).colorScheme.background
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.33),
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
event.originServerTs.localizedTime(context),
|
||||
style: TextStyle(fontSize: 14 * AppConfig.fontSizeFactor),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
row,
|
||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction))
|
||||
|
@ -27,21 +27,27 @@ class MessageContent extends StatelessWidget {
|
||||
final Color textColor;
|
||||
final void Function(Event)? onInfoTab;
|
||||
|
||||
const MessageContent(this.event,
|
||||
{this.onInfoTab, Key? key, required this.textColor})
|
||||
: super(key: key);
|
||||
const MessageContent(
|
||||
this.event, {
|
||||
this.onInfoTab,
|
||||
Key? key,
|
||||
required this.textColor,
|
||||
}) : super(key: key);
|
||||
|
||||
void _verifyOrRequestKey(BuildContext context) async {
|
||||
final l10n = L10n.of(context)!;
|
||||
if (event.content['can_request_session'] != true) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
event.type == EventTypes.Encrypted
|
||||
? l10n.needPantalaimonWarning
|
||||
: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(l10n),
|
||||
),
|
||||
)));
|
||||
event.type == EventTypes.Encrypted
|
||||
? l10n.needPantalaimonWarning
|
||||
: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(l10n),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final client = Matrix.of(context).client;
|
||||
@ -213,73 +219,83 @@ class MessageContent extends StatelessWidget {
|
||||
default:
|
||||
if (event.redacted) {
|
||||
return FutureBuilder<User?>(
|
||||
future: event.redactedBecause?.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.redactedAnEvent(snapshot.data
|
||||
?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname()),
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => onInfoTab!(event),
|
||||
);
|
||||
});
|
||||
future: event.redactedBecause?.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.redactedAnEvent(
|
||||
snapshot.data?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
),
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => onInfoTab!(event),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
final bigEmotes = event.onlyEmotes &&
|
||||
event.numberEmotes > 0 &&
|
||||
event.numberEmotes <= 10;
|
||||
return FutureBuilder<String>(
|
||||
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true),
|
||||
builder: (context, snapshot) {
|
||||
return LinkText(
|
||||
text: snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true),
|
||||
textStyle: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: textColor.withAlpha(150),
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: textColor.withAlpha(150),
|
||||
),
|
||||
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
);
|
||||
});
|
||||
future: event.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
return LinkText(
|
||||
text: snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: textColor.withAlpha(150),
|
||||
fontSize: bigEmotes ? fontSize * 3 : fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: textColor.withAlpha(150),
|
||||
),
|
||||
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
case EventTypes.CallInvite:
|
||||
return FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.startedACall(
|
||||
snapshot.data?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname()),
|
||||
icon: const Icon(Icons.phone_outlined),
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => onInfoTab!(event),
|
||||
);
|
||||
});
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.startedACall(
|
||||
snapshot.data?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
),
|
||||
icon: const Icon(Icons.phone_outlined),
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => onInfoTab!(event),
|
||||
);
|
||||
},
|
||||
);
|
||||
default:
|
||||
return FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.userSentUnknownEvent(
|
||||
snapshot.data?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
event.type),
|
||||
icon: const Icon(Icons.info_outlined),
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => onInfoTab!(event),
|
||||
);
|
||||
});
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.userSentUnknownEvent(
|
||||
snapshot.data?.calcDisplayname() ??
|
||||
event.senderFromMemoryOrFallback.calcDisplayname(),
|
||||
event.type,
|
||||
),
|
||||
icon: const Icon(Icons.info_outlined),
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => onInfoTab!(event),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,45 +46,51 @@ class MessageReactions extends StatelessWidget {
|
||||
|
||||
final reactionList = reactionMap.values.toList();
|
||||
reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
|
||||
return Wrap(spacing: 4.0, runSpacing: 4.0, children: [
|
||||
...reactionList
|
||||
.map(
|
||||
(r) => _Reaction(
|
||||
reactionKey: r.key,
|
||||
count: r.count,
|
||||
reacted: r.reacted,
|
||||
onTap: () {
|
||||
if (r.reacted) {
|
||||
final evt = allReactionEvents.firstWhereOrNull((e) =>
|
||||
e.senderId == e.room.client.userID &&
|
||||
e.content['m.relates_to']['key'] == r.key);
|
||||
if (evt != null) {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => evt.redactEvent(),
|
||||
return Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: [
|
||||
...reactionList
|
||||
.map(
|
||||
(r) => _Reaction(
|
||||
reactionKey: r.key,
|
||||
count: r.count,
|
||||
reacted: r.reacted,
|
||||
onTap: () {
|
||||
if (r.reacted) {
|
||||
final evt = allReactionEvents.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.senderId == e.room.client.userID &&
|
||||
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,
|
||||
).show(context),
|
||||
},
|
||||
onLongPress: () async => await _AdaptableReactorsDialog(
|
||||
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,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(count.toString(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
)),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
@ -133,11 +141,13 @@ class _Reaction extends StatelessWidget {
|
||||
if (renderKey.length > 10) {
|
||||
renderKey = renderKey.getRange(0, 9) + Characters('…');
|
||||
}
|
||||
content = Text('$renderKey $count',
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
));
|
||||
content = Text(
|
||||
'$renderKey $count',
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () => onTap != null ? onTap!() : null,
|
||||
|
@ -84,21 +84,22 @@ class ReplyContent extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
FutureBuilder<User?>(
|
||||
future: displayEvent.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
}),
|
||||
future: displayEvent.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
replyBody,
|
||||
],
|
||||
),
|
||||
|
@ -25,22 +25,23 @@ class StateMessage extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
child: FutureBuilder<String>(
|
||||
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -9,9 +9,11 @@ class VerificationRequestContent extends StatelessWidget {
|
||||
final Event event;
|
||||
final Timeline timeline;
|
||||
|
||||
const VerificationRequestContent(
|
||||
{required this.event, required this.timeline, Key? key})
|
||||
: super(key: key);
|
||||
const VerificationRequestContent({
|
||||
required this.event,
|
||||
required this.timeline,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -43,18 +45,22 @@ class VerificationRequestContent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(Icons.lock_outlined,
|
||||
color: canceled
|
||||
? Colors.red
|
||||
: (fullyDone ? Colors.green : Colors.grey)),
|
||||
Icon(
|
||||
Icons.lock_outlined,
|
||||
color: canceled
|
||||
? Colors.red
|
||||
: (fullyDone ? Colors.green : Colors.grey),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(canceled
|
||||
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
|
||||
: (fullyDone
|
||||
? L10n.of(context)!.verifySuccess
|
||||
: (started
|
||||
? L10n.of(context)!.loadingPleaseWait
|
||||
: L10n.of(context)!.newVerificationRequest)))
|
||||
Text(
|
||||
canceled
|
||||
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
|
||||
: (fullyDone
|
||||
? L10n.of(context)!.verifySuccess
|
||||
: (started
|
||||
? L10n.of(context)!.loadingPleaseWait
|
||||
: L10n.of(context)!.newVerificationRequest)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -39,7 +39,8 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
||||
} else {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = Uri.encodeComponent(
|
||||
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last);
|
||||
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
|
||||
);
|
||||
final file = File('${tempDir.path}/${fileName}_${videoFile.name}');
|
||||
if (await file.exists() == false) {
|
||||
await file.writeAsBytes(videoFile.bytes);
|
||||
@ -62,13 +63,17 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
||||
);
|
||||
}
|
||||
} on MatrixConnectionException catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(e.toLocalizedString(context)),
|
||||
));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toLocalizedString(context)),
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(e.toLocalizedString(context)),
|
||||
));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toLocalizedString(context)),
|
||||
),
|
||||
);
|
||||
Logs().w('Error while playing video', e, s);
|
||||
} finally {
|
||||
// Workaround for Chewie needs time to get the aspectRatio
|
||||
@ -120,14 +125,16 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.download_outlined),
|
||||
label: Text(
|
||||
_isDownloading
|
||||
? L10n.of(context)!.loadingPleaseWait
|
||||
: L10n.of(context)!.videoWithSize(
|
||||
widget.event.sizeString ?? '?MB'),
|
||||
widget.event.sizeString ?? '?MB',
|
||||
),
|
||||
),
|
||||
onPressed: _isDownloading ? null : _downloadAction,
|
||||
),
|
||||
|
@ -117,8 +117,10 @@ class InputBar extends StatelessWidget {
|
||||
}
|
||||
// aside of emote packs, also propose normal (tm) unicode emojis
|
||||
final matchingUnicodeEmojis = Emoji.all()
|
||||
.where((element) => [element.name, ...element.keywords]
|
||||
.any((element) => element.toLowerCase().contains(emoteSearch)))
|
||||
.where(
|
||||
(element) => [element.name, ...element.keywords]
|
||||
.any((element) => element.toLowerCase().contains(emoteSearch)),
|
||||
)
|
||||
.toList();
|
||||
// sort by the index of the search term in the name in order to have
|
||||
// best matches first
|
||||
@ -186,12 +188,14 @@ class InputBar extends StatelessWidget {
|
||||
.toLowerCase()
|
||||
.contains(roomSearch)) ||
|
||||
(state.content['alt_aliases'] is List &&
|
||||
state.content['alt_aliases'].any((l) =>
|
||||
l is String &&
|
||||
l
|
||||
.split(':')[0]
|
||||
.toLowerCase()
|
||||
.contains(roomSearch))))) ||
|
||||
state.content['alt_aliases'].any(
|
||||
(l) =>
|
||||
l is String &&
|
||||
l
|
||||
.split(':')[0]
|
||||
.toLowerCase()
|
||||
.contains(roomSearch),
|
||||
)))) ||
|
||||
(r.name.toLowerCase().contains(roomSearch))) {
|
||||
ret.add({
|
||||
'type': 'room',
|
||||
@ -226,8 +230,10 @@ class InputBar extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('/$command',
|
||||
style: const TextStyle(fontFamily: 'monospace')),
|
||||
Text(
|
||||
'/$command',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
Text(
|
||||
hint,
|
||||
maxLines: 1,
|
||||
@ -273,8 +279,8 @@ class InputBar extends StatelessWidget {
|
||||
child: suggestion['pack_avatar_url'] != null
|
||||
? Avatar(
|
||||
mxContent: Uri.tryParse(
|
||||
suggestion.tryGet<String>('pack_avatar_url') ??
|
||||
''),
|
||||
suggestion.tryGet<String>('pack_avatar_url') ?? '',
|
||||
),
|
||||
name: suggestion.tryGet<String>('pack_display_name'),
|
||||
size: size * 0.9,
|
||||
client: client,
|
||||
@ -397,23 +403,27 @@ class InputBar extends StatelessWidget {
|
||||
actions: !useShortCuts
|
||||
? {}
|
||||
: {
|
||||
NewLineIntent: CallbackAction(onInvoke: (i) {
|
||||
final val = controller!.value;
|
||||
final selection = val.selection.start;
|
||||
final messageWithoutNewLine =
|
||||
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
|
||||
controller!.value = TextEditingValue(
|
||||
text: messageWithoutNewLine,
|
||||
selection: TextSelection.fromPosition(
|
||||
TextPosition(offset: selection + 1),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
SubmitLineIntent: CallbackAction(onInvoke: (i) {
|
||||
onSubmitted!(controller!.text);
|
||||
return null;
|
||||
}),
|
||||
NewLineIntent: CallbackAction(
|
||||
onInvoke: (i) {
|
||||
final val = controller!.value;
|
||||
final selection = val.selection.start;
|
||||
final messageWithoutNewLine =
|
||||
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
|
||||
controller!.value = TextEditingValue(
|
||||
text: messageWithoutNewLine,
|
||||
selection: TextSelection.fromPosition(
|
||||
TextPosition(offset: selection + 1),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SubmitLineIntent: CallbackAction(
|
||||
onInvoke: (i) {
|
||||
onSubmitted!(controller!.text);
|
||||
return null;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: TypeAheadField<Map<String, String?>>(
|
||||
direction: AxisDirection.up,
|
||||
|
@ -18,23 +18,28 @@ class PinnedEvents extends StatelessWidget {
|
||||
const PinnedEvents(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
Future<void> _displayPinnedEventsDialog(
|
||||
BuildContext context, List<Event?> events) async {
|
||||
BuildContext context,
|
||||
List<Event?> events,
|
||||
) async {
|
||||
final eventId = events.length == 1
|
||||
? events.single?.eventId
|
||||
: await showConfirmationDialog<String>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.pinMessage,
|
||||
actions: events
|
||||
.map((event) => AlertDialogAction(
|
||||
key: event?.eventId ?? '',
|
||||
label: event?.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
) ??
|
||||
'UNKNOWN',
|
||||
))
|
||||
.toList());
|
||||
.map(
|
||||
(event) => AlertDialogAction(
|
||||
key: event?.eventId ?? '',
|
||||
label: event?.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
) ??
|
||||
'UNKNOWN',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (eventId != null) controller.scrollToEventId(eventId);
|
||||
}
|
||||
@ -54,87 +59,86 @@ class PinnedEvents extends StatelessWidget {
|
||||
return completer;
|
||||
});
|
||||
return FutureBuilder<List<Event?>>(
|
||||
future: Future.wait(completers.map((e) => e.future).toList()),
|
||||
builder: (context, snapshot) {
|
||||
final pinnedEvents = snapshot.data;
|
||||
final event = (pinnedEvents != null && pinnedEvents.isNotEmpty)
|
||||
? snapshot.data?.last
|
||||
: null;
|
||||
future: Future.wait(completers.map((e) => e.future).toList()),
|
||||
builder: (context, snapshot) {
|
||||
final pinnedEvents = snapshot.data;
|
||||
final event = (pinnedEvents != null && pinnedEvents.isNotEmpty)
|
||||
? snapshot.data?.last
|
||||
: null;
|
||||
|
||||
if (event == null || pinnedEvents == null) {
|
||||
return Container();
|
||||
}
|
||||
if (event == null || pinnedEvents == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: InkWell(
|
||||
onTap: () => _displayPinnedEventsDialog(
|
||||
context,
|
||||
pinnedEvents,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
splashRadius: 20,
|
||||
iconSize: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
tooltip: L10n.of(context)!.unpin,
|
||||
onPressed: controller.room
|
||||
?.canSendEvent(EventTypes.RoomPinnedEvents) ??
|
||||
false
|
||||
? () => controller.unpinEvent(event.eventId)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FutureBuilder<String>(
|
||||
future: event.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: InkWell(
|
||||
onTap: () => _displayPinnedEventsDialog(
|
||||
context,
|
||||
pinnedEvents,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
splashRadius: 20,
|
||||
iconSize: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
tooltip: L10n.of(context)!.unpin,
|
||||
onPressed: controller.room
|
||||
?.canSendEvent(EventTypes.RoomPinnedEvents) ??
|
||||
false
|
||||
? () => controller.unpinEvent(event.eventId)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FutureBuilder<String>(
|
||||
future: event.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: 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) {
|
||||
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,
|
||||
),
|
||||
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(),
|
||||
);
|
||||
}),
|
||||
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,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Builder(builder: (context) {
|
||||
if (!display) {
|
||||
return Container();
|
||||
}
|
||||
final proposals = proposeEmojis(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (!display) {
|
||||
return Container();
|
||||
}
|
||||
final proposals = proposeEmojis(
|
||||
controller.selectedEvents.first.plaintextBody,
|
||||
number: 25,
|
||||
languageCodes: EmojiProposalLanguageCodes.values.toSet());
|
||||
final emojis = proposals.isNotEmpty
|
||||
? proposals.map((e) => e.char).toList()
|
||||
: List<String>.from(AppEmojis.emojis);
|
||||
final allReactionEvents = controller.selectedEvents.first
|
||||
.aggregatedEvents(
|
||||
controller.timeline!, RelationshipTypes.reaction)
|
||||
.where((event) =>
|
||||
event.senderId == event.room.client.userID &&
|
||||
event.type == 'm.reaction');
|
||||
languageCodes: EmojiProposalLanguageCodes.values.toSet(),
|
||||
);
|
||||
final emojis = proposals.isNotEmpty
|
||||
? proposals.map((e) => e.char).toList()
|
||||
: List<String>.from(AppEmojis.emojis);
|
||||
final allReactionEvents = controller.selectedEvents.first
|
||||
.aggregatedEvents(
|
||||
controller.timeline!,
|
||||
RelationshipTypes.reaction,
|
||||
)
|
||||
.where(
|
||||
(event) =>
|
||||
event.senderId == event.room.client.userID &&
|
||||
event.type == 'm.reaction',
|
||||
);
|
||||
|
||||
for (final event in allReactionEvents) {
|
||||
try {
|
||||
emojis.remove(event.content['m.relates_to']['key']);
|
||||
} catch (_) {}
|
||||
}
|
||||
return Row(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
for (final event in allReactionEvents) {
|
||||
try {
|
||||
emojis.remove(event.content['m.relates_to']['key']);
|
||||
} catch (_) {}
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight:
|
||||
Radius.circular(AppConfig.borderRadius))),
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(right: 1),
|
||||
child: ListView.builder(
|
||||
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: () =>
|
||||
controller.pickEmojiReactionAction(allReactionEvents))
|
||||
]);
|
||||
}),
|
||||
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: () =>
|
||||
controller.pickEmojiReactionAction(allReactionEvents),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -130,7 +130,8 @@ class RecordingDialogState extends State<RecordingDialog> {
|
||||
.take(26)
|
||||
.toList()
|
||||
.reversed
|
||||
.map((amplitude) => Container(
|
||||
.map(
|
||||
(amplitude) => Container(
|
||||
margin: const EdgeInsets.only(left: 2),
|
||||
width: 4,
|
||||
decoration: BoxDecoration(
|
||||
@ -138,7 +139,9 @@ class RecordingDialogState extends State<RecordingDialog> {
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
height: maxDecibalWidth * (amplitude / 100)))
|
||||
height: maxDecibalWidth * (amplitude / 100),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
|
@ -33,10 +33,14 @@ class ReplyDisplay extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: controller.replyEvent != null
|
||||
? ReplyContent(controller.replyEvent!,
|
||||
timeline: controller.timeline!)
|
||||
: _EditContent(controller.editEvent
|
||||
?.getDisplayEvent(controller.timeline!)),
|
||||
? ReplyContent(
|
||||
controller.replyEvent!,
|
||||
timeline: controller.timeline!,
|
||||
)
|
||||
: _EditContent(
|
||||
controller.editEvent
|
||||
?.getDisplayEvent(controller.timeline!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -64,26 +68,27 @@ class _EditContent extends StatelessWidget {
|
||||
),
|
||||
Container(width: 15.0),
|
||||
FutureBuilder<String>(
|
||||
future: event.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
);
|
||||
}),
|
||||
future: event.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -33,11 +33,12 @@ class SendFileDialogState extends State<SendFileDialog> {
|
||||
MatrixImageFile? thumbnail;
|
||||
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
file = await file.resizeVideo();
|
||||
thumbnail = await file.getVideoThumbnail();
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
file = await file.resizeVideo();
|
||||
thumbnail = await file.getVideoThumbnail();
|
||||
},
|
||||
);
|
||||
}
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
widget.room
|
||||
@ -79,26 +80,29 @@ class SendFileDialogState extends State<SendFileDialog> {
|
||||
}
|
||||
Widget contentWidget;
|
||||
if (allFilesAreImages) {
|
||||
contentWidget = Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Flexible(
|
||||
child: Image.memory(
|
||||
widget.files.first.bytes,
|
||||
fit: BoxFit.contain,
|
||||
contentWidget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Image.memory(
|
||||
widget.files.first.bytes,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: origImage,
|
||||
onChanged: (v) => setState(() => origImage = v ?? false),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => setState(() => origImage = !origImage),
|
||||
child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'),
|
||||
),
|
||||
],
|
||||
)
|
||||
]);
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: origImage,
|
||||
onChanged: (v) => setState(() => origImage = v ?? false),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => setState(() => origImage = !origImage),
|
||||
child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
contentWidget = Text('$fileName ($sizeString)');
|
||||
}
|
||||
|
@ -28,12 +28,13 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
||||
final pack = stickerPacks[packSlugs[packIndex]]!;
|
||||
final filteredImagePackImageEntried = pack.images.entries.toList();
|
||||
if (searchFilter?.isNotEmpty ?? false) {
|
||||
filteredImagePackImageEntried.removeWhere((e) =>
|
||||
!(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
|
||||
(e.value.body
|
||||
?.toLowerCase()
|
||||
.contains(searchFilter!.toLowerCase()) ??
|
||||
false)));
|
||||
filteredImagePackImageEntried.removeWhere(
|
||||
(e) => !(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
|
||||
(e.value.body
|
||||
?.toLowerCase()
|
||||
.contains(searchFilter!.toLowerCase()) ??
|
||||
false)),
|
||||
);
|
||||
}
|
||||
final imageKeys =
|
||||
filteredImagePackImageEntried.map((e) => e.key).toList();
|
||||
@ -57,7 +58,8 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
||||
GridView.builder(
|
||||
itemCount: imageKeys.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 100),
|
||||
maxCrossAxisExtent: 100,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int imageIndex) {
|
||||
@ -127,10 +129,11 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
packBuilder,
|
||||
childCount: packSlugs.length,
|
||||
)),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
packBuilder,
|
||||
childCount: packSlugs.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -58,7 +58,8 @@ class ChatDetailsController extends State<ChatDetails> {
|
||||
);
|
||||
if (success.error == null) {
|
||||
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),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,7 +329,9 @@ class ChatDetailsController extends State<ChatDetails> {
|
||||
void requestMoreMembersAction() async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!);
|
||||
final participants = await showFutureLoadingDialog(
|
||||
context: context, future: () => room!.requestParticipants());
|
||||
context: context,
|
||||
future: () => room!.requestParticipants(),
|
||||
);
|
||||
if (participants.error == null) {
|
||||
setState(() => members = participants.result);
|
||||
}
|
||||
|
@ -43,364 +43,400 @@ class ChatDetailsView extends StatelessWidget {
|
||||
controller.members!.length < actualMembersCount;
|
||||
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||
return StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () =>
|
||||
VRouter.of(context).path.startsWith('/spaces/')
|
||||
? VRouter.of(context).pop()
|
||||
: VRouter.of(context)
|
||||
.toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
elevation: Theme.of(context).appBarTheme.elevation,
|
||||
expandedHeight: 300.0,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
actions: <Widget>[
|
||||
if (room.canonicalAlias.isNotEmpty)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.share,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
onPressed: () => FluffyShare.share(
|
||||
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
context),
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () =>
|
||||
VRouter.of(context).path.startsWith('/spaces/')
|
||||
? VRouter.of(context).pop()
|
||||
: VRouter.of(context)
|
||||
.toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
elevation: Theme.of(context).appBarTheme.elevation,
|
||||
expandedHeight: 300.0,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
actions: <Widget>[
|
||||
if (room.canonicalAlias.isNotEmpty)
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.share,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
onPressed: () => FluffyShare.share(
|
||||
AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
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)!),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: MaxWidthBody(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.members!.length +
|
||||
1 +
|
||||
(canRequestMoreMembers ? 1 : 0),
|
||||
itemBuilder: (BuildContext context, int i) => i == 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: room.canSendEvent(EventTypes.RoomTopic)
|
||||
? controller.setTopicAction
|
||||
: null,
|
||||
trailing: room.canSendEvent(EventTypes.RoomTopic)
|
||||
? Icon(
|
||||
Icons.edit_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
L10n.of(context)!.groupDescription,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: MaxWidthBody(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.members!.length +
|
||||
1 +
|
||||
(canRequestMoreMembers ? 1 : 0),
|
||||
itemBuilder: (BuildContext context, int i) => i == 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: room.canSendEvent(EventTypes.RoomTopic)
|
||||
? controller.setTopicAction
|
||||
: null,
|
||||
trailing: room.canSendEvent(EventTypes.RoomTopic)
|
||||
? Icon(
|
||||
Icons.edit_outlined,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.color,
|
||||
decorationColor: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.color,
|
||||
),
|
||||
onLinkTap: (url) =>
|
||||
UrlLauncher(context, url).launchUrl(),
|
||||
),
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
L10n.of(context)!.groupDescription,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
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(
|
||||
),
|
||||
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)
|
||||
.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(
|
||||
backgroundColor: Theme.of(context)
|
||||
.scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.people_outline_outlined),
|
||||
Icons.person_add_alt_1_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),
|
||||
title: Text(
|
||||
L10n.of(context)!.areGuestsAllowedToJoin,
|
||||
),
|
||||
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(
|
||||
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)!)),
|
||||
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(
|
||||
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,
|
||||
),
|
||||
title:
|
||||
Text(L10n.of(context)!.editChatPermissions),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.whoCanPerformWhichAction,
|
||||
),
|
||||
),
|
||||
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,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.grey,
|
||||
Icons.edit_attributes_outlined,
|
||||
),
|
||||
),
|
||||
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),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)),
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
permissionBatch,
|
||||
style: TextStyle(
|
||||
|
@ -19,179 +19,180 @@ class ChatEncryptionSettingsView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final room = controller.room;
|
||||
return StreamBuilder<Object>(
|
||||
stream: room.client.onSync.stream.where(
|
||||
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null),
|
||||
builder: (context, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
title: Text(L10n.of(context)!.endToEndEncryption),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
launchUrlString(AppConfig.encryptionTutorial),
|
||||
child: Text(L10n.of(context)!.help),
|
||||
),
|
||||
],
|
||||
stream: room.client.onSync.stream.where(
|
||||
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null,
|
||||
),
|
||||
builder: (context, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () =>
|
||||
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
title: Text(L10n.of(context)!.endToEndEncryption),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => launchUrlString(AppConfig.encryptionTutorial),
|
||||
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(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
secondary: CircleAvatar(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.lock_outlined)),
|
||||
title: Text(L10n.of(context)!.encryptThisChat),
|
||||
value: room.encrypted,
|
||||
onChanged: controller.enableEncryption,
|
||||
title: Text(L10n.of(context)!.encryptThisChat),
|
||||
value: room.encrypted,
|
||||
onChanged: controller.enableEncryption,
|
||||
),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/encryption.png',
|
||||
width: 212,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
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',
|
||||
width: 212,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (room.encrypted) ...[
|
||||
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(
|
||||
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),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) => FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (room.encrypted) ...[
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.deviceKeys,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) => FutureBuilder<
|
||||
List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
}
|
||||
final deviceKeys = snapshot.data!;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
SwitchListTile(
|
||||
value: !deviceKeys[i].blocked,
|
||||
activeColor: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
onChanged: (_) =>
|
||||
controller.toggleDeviceKey(deviceKeys[i]),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
deviceKeys[i].verified
|
||||
? Icons.verified_outlined
|
||||
: deviceKeys[i].blocked
|
||||
? Icons.block_outlined
|
||||
: Icons.info_outlined,
|
||||
color: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: deviceKeys[i].blocked
|
||||
? Colors.red
|
||||
: 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',
|
||||
);
|
||||
}
|
||||
final deviceKeys = snapshot.data!;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
SwitchListTile(
|
||||
value: !deviceKeys[i].blocked,
|
||||
activeColor: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
onChanged: (_) =>
|
||||
controller.toggleDeviceKey(deviceKeys[i]),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
deviceKeys[i].verified
|
||||
? Icons.verified_outlined
|
||||
: deviceKeys[i].blocked
|
||||
? Icons.block_outlined
|
||||
: Icons.info_outlined,
|
||||
color: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: deviceKeys[i].blocked
|
||||
? Colors.red
|
||||
: 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.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
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.encryptionNotEnabled,
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
deviceKeys[i].ed25519Key?.beautified ??
|
||||
L10n.of(context)!.unknownEncryptionAlgorithm,
|
||||
style: TextStyle(
|
||||
fontFamily: 'RobotoMono',
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
] 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 {
|
||||
final newServer = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context)!.changeTheHomeserver,
|
||||
context: context,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
prefixText: 'https://',
|
||||
hintText: Matrix.of(context).client.homeserver?.host,
|
||||
initialText: searchServer,
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false)
|
||||
]);
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context)!.changeTheHomeserver,
|
||||
context: context,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
prefixText: 'https://',
|
||||
hintText: Matrix.of(context).client.homeserver?.host,
|
||||
initialText: searchServer,
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (newServer == null) return;
|
||||
Store().setItem(_serverStoreNamespace, newServer.single);
|
||||
setState(() {
|
||||
@ -382,9 +384,11 @@ class ChatListController extends State<ChatList>
|
||||
}
|
||||
|
||||
void toggleSelection(String roomId) {
|
||||
setState(() => selectedRoomIds.contains(roomId)
|
||||
? selectedRoomIds.remove(roomId)
|
||||
: selectedRoomIds.add(roomId));
|
||||
setState(
|
||||
() => selectedRoomIds.contains(roomId)
|
||||
? selectedRoomIds.remove(roomId)
|
||||
: selectedRoomIds.add(roomId),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleUnread() async {
|
||||
@ -456,16 +460,17 @@ class ChatListController extends State<ChatList>
|
||||
|
||||
void setStatus() async {
|
||||
final input = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.setStatus,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context)!.statusExampleMessage,
|
||||
),
|
||||
]);
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.setStatus,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context)!.statusExampleMessage,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
@ -491,22 +496,23 @@ class ChatListController extends State<ChatList>
|
||||
|
||||
Future<void> addToSpace() async {
|
||||
final selectedSpace = await showConfirmationDialog<String>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.addToSpace,
|
||||
message: L10n.of(context)!.addToSpaceDescription,
|
||||
fullyCapitalizedForMaterial: false,
|
||||
actions: Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where((r) => r.isSpace)
|
||||
.map(
|
||||
(space) => AlertDialogAction(
|
||||
key: space.id,
|
||||
label: space
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
context: context,
|
||||
title: L10n.of(context)!.addToSpace,
|
||||
message: L10n.of(context)!.addToSpaceDescription,
|
||||
fullyCapitalizedForMaterial: false,
|
||||
actions: Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where((r) => r.isSpace)
|
||||
.map(
|
||||
(space) => AlertDialogAction(
|
||||
key: space.id,
|
||||
label: space
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
if (selectedSpace == null) return;
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
@ -532,14 +538,19 @@ class ChatListController extends State<ChatList>
|
||||
}
|
||||
|
||||
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(
|
||||
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite);
|
||||
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite,
|
||||
);
|
||||
|
||||
bool get anySelectedRoomNotMuted => selectedRoomIds.any((roomId) =>
|
||||
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
|
||||
PushRuleState.notify);
|
||||
bool get anySelectedRoomNotMuted => selectedRoomIds.any(
|
||||
(roomId) =>
|
||||
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
|
||||
PushRuleState.notify,
|
||||
);
|
||||
|
||||
bool waitForFirstSync = false;
|
||||
|
||||
@ -624,9 +635,10 @@ class ChatListController extends State<ChatList>
|
||||
switch (action) {
|
||||
case EditBundleAction.addToBundle:
|
||||
final bundle = await showTextInputDialog(
|
||||
context: context,
|
||||
title: l10n.bundleName,
|
||||
textFields: [DialogTextField(hintText: l10n.bundleName)]);
|
||||
context: context,
|
||||
title: l10n.bundleName,
|
||||
textFields: [DialogTextField(hintText: l10n.bundleName)],
|
||||
);
|
||||
if (bundle == null || bundle.isEmpty || bundle.single.isEmpty) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
@ -46,247 +46,248 @@ class ChatListViewBody extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
child: StreamBuilder(
|
||||
key: ValueKey(client.userID.toString() +
|
||||
key: ValueKey(
|
||||
client.userID.toString() +
|
||||
controller.activeFilter.toString() +
|
||||
controller.activeSpaceId.toString()),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
if (controller.activeFilter == ActiveFilter.spaces &&
|
||||
!controller.isSearchMode) {
|
||||
return SpaceView(
|
||||
controller,
|
||||
scrollController: controller.scrollController,
|
||||
key: Key(controller.activeSpaceId ?? 'Spaces'),
|
||||
);
|
||||
}
|
||||
if (controller.waitForFirstSync && client.prevBatch != null) {
|
||||
final rooms = controller.filteredRooms;
|
||||
final displayStoriesHeader = {
|
||||
ActiveFilter.allChats,
|
||||
ActiveFilter.messages,
|
||||
}.contains(controller.activeFilter) &&
|
||||
client.storiesRooms.isNotEmpty;
|
||||
return ListView.builder(
|
||||
controller: controller.scrollController,
|
||||
// add +1 space below in order to properly scroll below the spaces bar
|
||||
itemCount: rooms.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == 0) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (controller.isSearchMode) ...[
|
||||
SearchTitle(
|
||||
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(),
|
||||
controller.activeSpaceId.toString(),
|
||||
),
|
||||
stream: client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, _) {
|
||||
if (controller.activeFilter == ActiveFilter.spaces &&
|
||||
!controller.isSearchMode) {
|
||||
return SpaceView(
|
||||
controller,
|
||||
scrollController: controller.scrollController,
|
||||
key: Key(controller.activeSpaceId ?? 'Spaces'),
|
||||
);
|
||||
}
|
||||
if (controller.waitForFirstSync && client.prevBatch != null) {
|
||||
final rooms = controller.filteredRooms;
|
||||
final displayStoriesHeader = {
|
||||
ActiveFilter.allChats,
|
||||
ActiveFilter.messages,
|
||||
}.contains(controller.activeFilter) &&
|
||||
client.storiesRooms.isNotEmpty;
|
||||
return ListView.builder(
|
||||
controller: controller.scrollController,
|
||||
// add +1 space below in order to properly scroll below the spaces bar
|
||||
itemCount: rooms.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == 0) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (controller.isSearchMode) ...[
|
||||
SearchTitle(
|
||||
title: L10n.of(context)!.publicRooms,
|
||||
icon: const Icon(Icons.explore_outlined),
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: controller.isTorBrowser ? 64 : 0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(L10n.of(context)!.dehydrateTor),
|
||||
subtitle:
|
||||
Text(L10n.of(context)!.dehydrateTorLong),
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right_outlined),
|
||||
onTap: controller.dehydrate,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.isSearchMode)
|
||||
SearchTitle(
|
||||
title: L10n.of(context)!.chats,
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
),
|
||||
if (rooms.isEmpty && !controller.isSearchMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/start_chat.png',
|
||||
height: 256,
|
||||
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],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
i--;
|
||||
if (!rooms[i]
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
||||
.toLowerCase()
|
||||
.contains(
|
||||
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),
|
||||
if (displayStoriesHeader)
|
||||
StoriesHeader(
|
||||
key: const Key('stories_header'),
|
||||
filter: controller.searchController.text,
|
||||
),
|
||||
const ConnectionStatusHeader(),
|
||||
AnimatedContainer(
|
||||
height: controller.isTorBrowser ? 64 : 0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(L10n.of(context)!.dehydrateTor),
|
||||
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: controller.dehydrate,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 36),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: subtitleColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
if (controller.isSearchMode)
|
||||
SearchTitle(
|
||||
title: L10n.of(context)!.chats,
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: subtitleColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
if (rooms.isEmpty && !controller.isSearchMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/start_chat.png',
|
||||
height: 256,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: subtitleColor,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
height: 12,
|
||||
margin: const EdgeInsets.only(right: 22),
|
||||
);
|
||||
}
|
||||
i--;
|
||||
if (!rooms[i]
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
||||
.toLowerCase()
|
||||
.contains(
|
||||
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(
|
||||
tooltip: L10n.of(context)!.toggleUnread,
|
||||
icon: Icon(controller.anySelectedRoomNotMarkedUnread
|
||||
? Icons.mark_chat_read_outlined
|
||||
: Icons.mark_chat_unread_outlined),
|
||||
icon: Icon(
|
||||
controller.anySelectedRoomNotMarkedUnread
|
||||
? Icons.mark_chat_read_outlined
|
||||
: Icons.mark_chat_unread_outlined,
|
||||
),
|
||||
onPressed: controller.toggleUnread,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.toggleFavorite,
|
||||
icon: Icon(controller.anySelectedRoomNotFavorite
|
||||
? Icons.push_pin_outlined
|
||||
: Icons.push_pin),
|
||||
icon: Icon(
|
||||
controller.anySelectedRoomNotFavorite
|
||||
? Icons.push_pin_outlined
|
||||
: Icons.push_pin,
|
||||
),
|
||||
onPressed: controller.toggleFavouriteRoom,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(controller.anySelectedRoomNotMuted
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_outlined),
|
||||
icon: Icon(
|
||||
controller.anySelectedRoomNotMuted
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_outlined,
|
||||
),
|
||||
tooltip: L10n.of(context)!.toggleMuted,
|
||||
onPressed: controller.toggleMuted,
|
||||
),
|
||||
|
@ -38,15 +38,16 @@ class ChatListItem extends StatelessWidget {
|
||||
if (activeChat) return;
|
||||
if (room.membership == Membership.invite) {
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
}
|
||||
|
||||
@ -107,7 +108,9 @@ class ChatListItem extends StatelessWidget {
|
||||
);
|
||||
if (confirmed == OkCancelResult.cancel) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context, future: () => room.leave());
|
||||
context: context,
|
||||
future: () => room.leave(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -183,7 +186,8 @@ class ChatListItem extends StatelessWidget {
|
||||
if (room.isFavourite)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: room.notificationCount > 0 ? 4.0 : 0.0),
|
||||
right: room.notificationCount > 0 ? 4.0 : 0.0,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.push_pin,
|
||||
size: 16,
|
||||
@ -282,7 +286,8 @@ class ChatListItem extends StatelessWidget {
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
AnimatedContainer(
|
||||
|
@ -109,56 +109,60 @@ class ChatListView extends StatelessWidget {
|
||||
children: [
|
||||
if (FluffyThemes.isColumnMode(context) &&
|
||||
FluffyThemes.getDisplayNavigationRail(context)) ...[
|
||||
Builder(builder: (context) {
|
||||
final allSpaces = client.rooms.where((room) => room.isSpace);
|
||||
final rootSpaces = allSpaces
|
||||
.where(
|
||||
(space) => !allSpaces.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final destinations = getNavigationDestinations(context);
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final allSpaces =
|
||||
client.rooms.where((room) => room.isSpace);
|
||||
final rootSpaces = allSpaces
|
||||
.where(
|
||||
(space) => !allSpaces.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final destinations = getNavigationDestinations(context);
|
||||
|
||||
return SizedBox(
|
||||
width: 64,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: rootSpaces.length + destinations.length,
|
||||
itemBuilder: (context, i) {
|
||||
if (i < destinations.length) {
|
||||
return SizedBox(
|
||||
width: 64,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: rootSpaces.length + destinations.length,
|
||||
itemBuilder: (context, i) {
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
toolTip: rootSpaces[i].getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
size: 32,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
isSelected: isSelected,
|
||||
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(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
|
@ -20,11 +20,13 @@ class ClientChooserButton extends StatelessWidget {
|
||||
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
||||
final matrix = Matrix.of(context);
|
||||
final bundles = matrix.accountBundles.keys.toList()
|
||||
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1);
|
||||
..sort(
|
||||
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1,
|
||||
);
|
||||
return <PopupMenuEntry<Object>>[
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.newStory,
|
||||
@ -142,7 +144,9 @@ class ClientChooserButton extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
onPressed: () => controller.editBundlesForAccount(
|
||||
client.userID, bundle),
|
||||
client.userID,
|
||||
bundle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -270,9 +274,12 @@ class ClientChooserButton extends StatelessWidget {
|
||||
break;
|
||||
case SettingsAction.invite:
|
||||
FluffyShare.share(
|
||||
L10n.of(context)!.inviteText(Matrix.of(context).client.userID!,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'),
|
||||
context);
|
||||
L10n.of(context)!.inviteText(
|
||||
Matrix.of(context).client.userID!,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat',
|
||||
),
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case SettingsAction.settings:
|
||||
VRouter.of(context).to('/settings');
|
||||
@ -290,11 +297,13 @@ class ClientChooserButton extends StatelessWidget {
|
||||
BuildContext context,
|
||||
) {
|
||||
final bundles = matrix.accountBundles.keys.toList()
|
||||
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1);
|
||||
..sort(
|
||||
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1,
|
||||
);
|
||||
// beginning from end if negative
|
||||
if (index < 0) {
|
||||
int clientCount = 0;
|
||||
@ -320,11 +329,13 @@ class ClientChooserButton extends StatelessWidget {
|
||||
int index = 0;
|
||||
|
||||
final bundles = matrix.accountBundles.keys.toList()
|
||||
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1);
|
||||
..sort(
|
||||
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1,
|
||||
);
|
||||
for (final bundleName in bundles) {
|
||||
final bundle = matrix.accountBundles[bundleName];
|
||||
if (bundle == null) return null;
|
||||
|
@ -48,17 +48,18 @@ class NaviRailItem extends StatelessWidget {
|
||||
onPressed: onTap,
|
||||
tooltip: toolTip,
|
||||
icon: Material(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: isSelected ? selectedIcon ?? icon : icon,
|
||||
)),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: isSelected ? selectedIcon ?? icon : icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -45,13 +45,15 @@ class SearchTitle extends StatelessWidget {
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 16),
|
||||
Text(title,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (trailing != null)
|
||||
Expanded(
|
||||
child: Align(
|
||||
|
@ -53,11 +53,14 @@ class _SpaceViewState extends State<SpaceView> {
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await client.joinRoom(spaceChild.roomId,
|
||||
serverName: space?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
(child) => child.roomId == spaceChild.roomId)
|
||||
?.via);
|
||||
await client.joinRoom(
|
||||
spaceChild.roomId,
|
||||
serverName: space?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
(child) => child.roomId == spaceChild.roomId,
|
||||
)
|
||||
?.via,
|
||||
);
|
||||
if (client.getRoomById(spaceChild.roomId) == null) {
|
||||
// Wait for room actually appears in sync
|
||||
await client.waitForRoomInSync(spaceChild.roomId, join: true);
|
||||
@ -78,8 +81,10 @@ class _SpaceViewState extends State<SpaceView> {
|
||||
VRouter.of(context).toSegments(['rooms', spaceChild.roomId]);
|
||||
}
|
||||
|
||||
void _onSpaceChildContextMenu(
|
||||
[SpaceRoomsChunk? spaceChild, Room? room]) async {
|
||||
void _onSpaceChildContextMenu([
|
||||
SpaceRoomsChunk? spaceChild,
|
||||
Room? room,
|
||||
]) async {
|
||||
final client = Matrix.of(context).client;
|
||||
final activeSpaceId = widget.controller.activeSpaceId;
|
||||
final activeSpace =
|
||||
@ -169,8 +174,10 @@ class _SpaceViewState extends State<SpaceView> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(L10n.of(context)!
|
||||
.numChats(rootSpace.spaceChildren.length.toString())),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!
|
||||
.numChats(rootSpace.spaceChildren.length.toString()),
|
||||
),
|
||||
onTap: () => widget.controller.setActiveSpace(rootSpace.id),
|
||||
onLongPress: () => _onSpaceChildContextMenu(null, rootSpace),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
@ -180,166 +187,166 @@ class _SpaceViewState extends State<SpaceView> {
|
||||
);
|
||||
}
|
||||
return FutureBuilder<GetSpaceHierarchyResponse>(
|
||||
future: getFuture(activeSpaceId),
|
||||
builder: (context, snapshot) {
|
||||
final response = snapshot.data;
|
||||
final error = snapshot.error;
|
||||
if (error != null) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(error.toLocalizedString(context)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _refresh,
|
||||
icon: const Icon(Icons.refresh_outlined),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
if (response == null) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
final parentSpace = allSpaces.firstWhereOrNull((space) => space
|
||||
.spaceChildren
|
||||
.any((child) => child.roomId == activeSpaceId));
|
||||
final spaceChildren = response.rooms;
|
||||
final canLoadMore = response.nextBatch != null;
|
||||
return VWidgetGuard(
|
||||
onSystemPop: (redirector) async {
|
||||
if (parentSpace != null) {
|
||||
widget.controller.setActiveSpace(parentSpace.id);
|
||||
redirector.stopRedirection();
|
||||
return;
|
||||
future: getFuture(activeSpaceId),
|
||||
builder: (context, snapshot) {
|
||||
final response = snapshot.data;
|
||||
final error = snapshot.error;
|
||||
if (error != null) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(error.toLocalizedString(context)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _refresh,
|
||||
icon: const Icon(Icons.refresh_outlined),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
if (response == null) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
final parentSpace = allSpaces.firstWhereOrNull(
|
||||
(space) =>
|
||||
space.spaceChildren.any((child) => child.roomId == activeSpaceId),
|
||||
);
|
||||
final spaceChildren = response.rooms;
|
||||
final canLoadMore = response.nextBatch != null;
|
||||
return VWidgetGuard(
|
||||
onSystemPop: (redirector) async {
|
||||
if (parentSpace != null) {
|
||||
widget.controller.setActiveSpace(parentSpace.id);
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
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--;
|
||||
if (canLoadMore && i == spaceChildren.length) {
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context)!.loadMore),
|
||||
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(
|
||||
i--;
|
||||
if (canLoadMore && i == spaceChildren.length) {
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context)!.loadMore),
|
||||
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,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
spaceChild.name ??
|
||||
spaceChild.canonicalAlias ??
|
||||
L10n.of(context)!.chat,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (!isSpace) ...[
|
||||
const Icon(
|
||||
Icons.people_outline,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
spaceChild.numJoinedMembers.toString(),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
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,
|
||||
name: spaceChild.name,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
spaceChild.name ??
|
||||
spaceChild.canonicalAlias ??
|
||||
L10n.of(context)!.chat,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
onTap: () => _onJoinSpaceChild(spaceChild),
|
||||
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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (!isSpace) ...[
|
||||
const Icon(
|
||||
Icons.people_outline,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
spaceChild.numJoinedMembers.toString(),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
onTap: () => _onJoinSpaceChild(spaceChild),
|
||||
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> {
|
||||
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
||||
void editPowerLevel(BuildContext context, String key, int currentLevel,
|
||||
{String? category}) async {
|
||||
void editPowerLevel(
|
||||
BuildContext context,
|
||||
String key,
|
||||
int currentLevel, {
|
||||
String? category,
|
||||
}) async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!)!;
|
||||
if (!room.canSendEvent(EventTypes.RoomPowerLevels)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.noPermission)));
|
||||
SnackBar(content: Text(L10n.of(context)!.noPermission)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final newLevel = await showPermissionChooser(
|
||||
@ -36,7 +41,8 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
||||
);
|
||||
if (newLevel == null) return;
|
||||
final content = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels)!.content);
|
||||
room.getState(EventTypes.RoomPowerLevels)!.content,
|
||||
);
|
||||
if (category != null) {
|
||||
if (!content.containsKey(category)) {
|
||||
content[category] = <String, dynamic>{};
|
||||
@ -74,10 +80,13 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
||||
title: L10n.of(context)!.replaceRoomWithNewerVersion,
|
||||
actions: capabilities.mRoomVersions!.available.entries
|
||||
.where((r) => r.key != roomVersion)
|
||||
.map((version) => AlertDialogAction(
|
||||
.map(
|
||||
(version) => AlertDialogAction(
|
||||
key: version.key,
|
||||
label:
|
||||
'${version.key} (${version.value.toString().split('.').last})'))
|
||||
'${version.key} (${version.value.toString().split('.').last})',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
if (newVersion == null ||
|
||||
|
@ -41,7 +41,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
return Center(child: Text(L10n.of(context)!.noRoomsFound));
|
||||
}
|
||||
final powerLevelsContent = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels)!.content);
|
||||
room.getState(EventTypes.RoomPowerLevels)!.content,
|
||||
);
|
||||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||
..removeWhere((k, v) => v is! int);
|
||||
final eventsPowerLevels =
|
||||
@ -57,7 +58,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
permissionKey: entry.key,
|
||||
permission: entry.value,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, entry.key, entry.value),
|
||||
context,
|
||||
entry.key,
|
||||
entry.value,
|
||||
),
|
||||
),
|
||||
const Divider(thickness: 1),
|
||||
ListTile(
|
||||
@ -69,21 +73,26 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
const key = 'rooms';
|
||||
final int value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent['notifications']['rooms'] ?? 0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
permission: value,
|
||||
category: 'notifications',
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, key, value,
|
||||
category: 'notifications'),
|
||||
);
|
||||
}),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
const key = 'rooms';
|
||||
final int value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent['notifications']['rooms'] ?? 0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
permission: value,
|
||||
category: 'notifications',
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context,
|
||||
key,
|
||||
value,
|
||||
category: 'notifications',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
@ -100,8 +109,11 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
category: 'events',
|
||||
permission: entry.value,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context, entry.key, entry.value,
|
||||
category: 'events'),
|
||||
context,
|
||||
entry.key,
|
||||
entry.value,
|
||||
category: 'events',
|
||||
),
|
||||
),
|
||||
if (room.canSendEvent(EventTypes.RoomTombstone)) ...{
|
||||
const Divider(thickness: 1),
|
||||
@ -110,8 +122,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
final String roomVersion = room
|
||||
.getState(EventTypes.RoomCreate)!
|
||||
@ -120,7 +134,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${L10n.of(context)!.roomVersion}: $roomVersion'),
|
||||
'${L10n.of(context)!.roomVersion}: $roomVersion',
|
||||
),
|
||||
onTap: () =>
|
||||
controller.updateRoomAction(snapshot.data!),
|
||||
);
|
||||
|
@ -118,8 +118,9 @@ class ConnectPageController extends State<ConnectPage> {
|
||||
List<IdentityProvider>? get identityProviders {
|
||||
final loginTypes = _rawLoginTypes;
|
||||
if (loginTypes == null) return null;
|
||||
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere((flow) =>
|
||||
flow['type'] == AuthenticationTypes.sso)['identity_providers'];
|
||||
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
|
||||
(flow) => flow['type'] == AuthenticationTypes.sso,
|
||||
)['identity_providers'];
|
||||
final list = (rawProviders as List)
|
||||
.map((json) => IdentityProvider.fromJson(json))
|
||||
.toList();
|
||||
@ -163,9 +164,11 @@ class ConnectPageController extends State<ConnectPage> {
|
||||
RequestType.GET,
|
||||
'/client/r0/login',
|
||||
)
|
||||
.then((loginTypes) => setState(() {
|
||||
_rawLoginTypes = loginTypes;
|
||||
}));
|
||||
.then(
|
||||
(loginTypes) => setState(() {
|
||||
_rawLoginTypes = loginTypes;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,17 +174,20 @@ class ConnectPageView extends StatelessWidget {
|
||||
)
|
||||
: Image.network(
|
||||
Uri.parse(identityProviders.single.icon!)
|
||||
.getDownloadLink(Matrix.of(context)
|
||||
.getLoginClient())
|
||||
.getDownloadLink(
|
||||
Matrix.of(context).getLoginClient(),
|
||||
)
|
||||
.toString(),
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
onPressed: () => controller
|
||||
.ssoLoginAction(identityProviders.single.id!),
|
||||
label: Text(identityProviders.single.name ??
|
||||
identityProviders.single.brand ??
|
||||
L10n.of(context)!.loginWithOneClick),
|
||||
label: Text(
|
||||
identityProviders.single.name ??
|
||||
identityProviders.single.brand ??
|
||||
L10n.of(context)!.loginWithOneClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Wrap(
|
||||
|
@ -37,7 +37,8 @@ class SsoButton extends StatelessWidget {
|
||||
: Image.network(
|
||||
Uri.parse(identityProvider.icon!)
|
||||
.getDownloadLink(
|
||||
Matrix.of(context).getLoginClient())
|
||||
Matrix.of(context).getLoginClient(),
|
||||
)
|
||||
.toString(),
|
||||
width: 32,
|
||||
height: 32,
|
||||
|
@ -35,7 +35,8 @@ class DevicesSettingsView extends StatelessWidget {
|
||||
}
|
||||
if (!snapshot.hasData || controller.devices == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.notThisDevice.length + 1,
|
||||
@ -63,12 +64,14 @@ class DevicesSettingsView extends StatelessWidget {
|
||||
),
|
||||
trailing: controller.loadingDeletingDevices
|
||||
? const CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2)
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: const Icon(Icons.delete_outline),
|
||||
onTap: controller.loadingDeletingDevices
|
||||
? null
|
||||
: () => controller.removeDevicesAction(
|
||||
controller.notThisDevice),
|
||||
controller.notThisDevice,
|
||||
),
|
||||
)
|
||||
else
|
||||
Center(
|
||||
|
@ -135,8 +135,9 @@ class UserDeviceListItem extends StatelessWidget {
|
||||
),
|
||||
subtitle: Text(
|
||||
L10n.of(context)!.lastActiveAgo(
|
||||
DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0)
|
||||
.localizedTimeShort(context)),
|
||||
DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0)
|
||||
.localizedTimeShort(context),
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.w300),
|
||||
),
|
||||
);
|
||||
|
@ -36,9 +36,12 @@ import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'pip/pip_view.dart';
|
||||
|
||||
class _StreamView extends StatelessWidget {
|
||||
const _StreamView(this.wrappedStream,
|
||||
{Key? key, this.mainView = false, required this.matrixClient})
|
||||
: super(key: key);
|
||||
const _StreamView(
|
||||
this.wrappedStream, {
|
||||
Key? key,
|
||||
this.mainView = false,
|
||||
required this.matrixClient,
|
||||
}) : super(key: key);
|
||||
|
||||
final WrappedMediaStream wrappedStream;
|
||||
final Client matrixClient;
|
||||
@ -67,43 +70,48 @@ class _StreamView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black54,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
if (videoMuted)
|
||||
Container(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
if (!videoMuted)
|
||||
RTCVideoView(
|
||||
// yes, it must explicitly be casted even though I do not feel
|
||||
// comfortable with it...
|
||||
wrappedStream.renderer as RTCVideoRenderer,
|
||||
mirror: mirrored,
|
||||
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
),
|
||||
if (videoMuted)
|
||||
Positioned(
|
||||
child: Avatar(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black54,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
if (videoMuted)
|
||||
Container(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
if (!videoMuted)
|
||||
RTCVideoView(
|
||||
// yes, it must explicitly be casted even though I do not feel
|
||||
// comfortable with it...
|
||||
wrappedStream.renderer as RTCVideoRenderer,
|
||||
mirror: mirrored,
|
||||
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
),
|
||||
if (videoMuted)
|
||||
Positioned(
|
||||
child: Avatar(
|
||||
mxContent: avatarUrl,
|
||||
name: displayName,
|
||||
size: mainView ? 96 : 48,
|
||||
client: matrixClient,
|
||||
// textSize: mainView ? 36 : 24,
|
||||
// matrixClient: matrixClient,
|
||||
)),
|
||||
if (!isScreenSharing)
|
||||
Positioned(
|
||||
left: 4.0,
|
||||
bottom: 4.0,
|
||||
child: Icon(audioMuted ? Icons.mic_off : Icons.mic,
|
||||
color: Colors.white, size: 18.0),
|
||||
)
|
||||
],
|
||||
));
|
||||
),
|
||||
),
|
||||
if (!isScreenSharing)
|
||||
Positioned(
|
||||
left: 4.0,
|
||||
bottom: 4.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 Client client;
|
||||
|
||||
const Calling(
|
||||
{required this.context,
|
||||
required this.call,
|
||||
required this.client,
|
||||
required this.callId,
|
||||
this.onClear,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
const Calling({
|
||||
required this.context,
|
||||
required this.call,
|
||||
required this.client,
|
||||
required this.callId,
|
||||
this.onClear,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
MyCallingPage createState() => MyCallingPage();
|
||||
@ -206,7 +214,8 @@ class MyCallingPage extends State<Calling> {
|
||||
event == CallEvent.kRemoteHoldUnhold) {
|
||||
setState(() {});
|
||||
Logs().i(
|
||||
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}');
|
||||
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
|
||||
);
|
||||
}
|
||||
});
|
||||
_state = call.state;
|
||||
@ -239,7 +248,9 @@ class MyCallingPage extends State<Calling> {
|
||||
|
||||
void _resizeLocalVideo(Orientation orientation) {
|
||||
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
|
||||
? const EdgeInsets.only(top: 20.0, right: 20.0)
|
||||
: EdgeInsets.zero;
|
||||
@ -304,8 +315,9 @@ class MyCallingPage extends State<Calling> {
|
||||
foregroundTaskOptions: const ForegroundTaskOptions(),
|
||||
);
|
||||
FlutterForegroundTask.startService(
|
||||
notificationTitle: L10n.of(context)!.screenSharingTitle,
|
||||
notificationText: L10n.of(context)!.screenSharingDetail);
|
||||
notificationTitle: L10n.of(context)!.screenSharingTitle,
|
||||
notificationText: L10n.of(context)!.screenSharingDetail,
|
||||
);
|
||||
} else {
|
||||
FlutterForegroundTask.stopService();
|
||||
}
|
||||
@ -331,7 +343,8 @@ class MyCallingPage extends State<Calling> {
|
||||
void _switchCamera() async {
|
||||
if (call.localUserMediaStream != null) {
|
||||
await Helper.switchCamera(
|
||||
call.localUserMediaStream!.stream!.getVideoTracks()[0]);
|
||||
call.localUserMediaStream!.stream!.getVideoTracks()[0],
|
||||
);
|
||||
if (PlatformInfos.isMobile) {
|
||||
call.facingMode == 'user'
|
||||
? call.facingMode = 'environment'
|
||||
@ -473,22 +486,27 @@ class MyCallingPage extends State<Calling> {
|
||||
} else if (call.remoteOnHold) {
|
||||
title = 'You held the call.';
|
||||
}
|
||||
stackWidgets.add(Center(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
const Icon(
|
||||
Icons.pause,
|
||||
size: 48.0,
|
||||
color: Colors.white,
|
||||
stackWidgets.add(
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
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;
|
||||
}
|
||||
|
||||
@ -502,10 +520,15 @@ class MyCallingPage extends State<Calling> {
|
||||
}
|
||||
|
||||
if (primaryStream != null) {
|
||||
stackWidgets.add(Center(
|
||||
child: _StreamView(primaryStream,
|
||||
mainView: true, matrixClient: widget.client),
|
||||
));
|
||||
stackWidgets.add(
|
||||
Center(
|
||||
child: _StreamView(
|
||||
primaryStream,
|
||||
mainView: true,
|
||||
matrixClient: widget.client,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isFloating || !connected) {
|
||||
@ -522,47 +545,58 @@ class MyCallingPage extends State<Calling> {
|
||||
|
||||
if (call.remoteScreenSharingStream != null) {
|
||||
final remoteUserMediaStream = call.remoteUserMediaStream;
|
||||
secondaryStreamViews.add(SizedBox(
|
||||
width: _localVideoWidth,
|
||||
height: _localVideoHeight,
|
||||
child: _StreamView(remoteUserMediaStream!, matrixClient: widget.client),
|
||||
));
|
||||
secondaryStreamViews.add(
|
||||
SizedBox(
|
||||
width: _localVideoWidth,
|
||||
height: _localVideoHeight,
|
||||
child:
|
||||
_StreamView(remoteUserMediaStream!, matrixClient: widget.client),
|
||||
),
|
||||
);
|
||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||
}
|
||||
|
||||
final localStream =
|
||||
call.localUserMediaStream ?? call.localScreenSharingStream;
|
||||
if (localStream != null && !isFloating) {
|
||||
secondaryStreamViews.add(SizedBox(
|
||||
width: _localVideoWidth,
|
||||
height: _localVideoHeight,
|
||||
child: _StreamView(localStream, matrixClient: widget.client),
|
||||
));
|
||||
secondaryStreamViews.add(
|
||||
SizedBox(
|
||||
width: _localVideoWidth,
|
||||
height: _localVideoHeight,
|
||||
child: _StreamView(localStream, matrixClient: widget.client),
|
||||
),
|
||||
);
|
||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||
}
|
||||
|
||||
if (call.localScreenSharingStream != null && !isFloating) {
|
||||
secondaryStreamViews.add(SizedBox(
|
||||
width: _localVideoWidth,
|
||||
height: _localVideoHeight,
|
||||
child: _StreamView(call.remoteUserMediaStream!,
|
||||
matrixClient: widget.client),
|
||||
));
|
||||
secondaryStreamViews.add(
|
||||
SizedBox(
|
||||
width: _localVideoWidth,
|
||||
height: _localVideoHeight,
|
||||
child: _StreamView(
|
||||
call.remoteUserMediaStream!,
|
||||
matrixClient: widget.client,
|
||||
),
|
||||
),
|
||||
);
|
||||
secondaryStreamViews.add(const SizedBox(height: 10));
|
||||
}
|
||||
|
||||
if (secondaryStreamViews.isNotEmpty) {
|
||||
stackWidgets.add(Container(
|
||||
padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Container(
|
||||
width: _localVideoWidth,
|
||||
margin: _localVideoMargin,
|
||||
child: Column(
|
||||
children: secondaryStreamViews,
|
||||
stackWidgets.add(
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Container(
|
||||
width: _localVideoWidth,
|
||||
margin: _localVideoMargin,
|
||||
child: Column(
|
||||
children: secondaryStreamViews,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
return stackWidgets;
|
||||
@ -570,27 +604,31 @@ class MyCallingPage extends State<Calling> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PIPView(builder: (context, isFloating) {
|
||||
return Scaffold(
|
||||
return PIPView(
|
||||
builder: (context, isFloating) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: !isFloating,
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: SizedBox(
|
||||
width: 320.0,
|
||||
height: 150.0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _buildActionButtons(isFloating))),
|
||||
width: 320.0,
|
||||
height: 150.0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _buildActionButtons(isFloating),
|
||||
),
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
builder: (BuildContext context, Orientation orientation) {
|
||||
return Container(
|
||||
builder: (BuildContext context, Orientation orientation) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black87,
|
||||
),
|
||||
child: Stack(children: [
|
||||
..._buildContent(orientation, isFloating),
|
||||
if (!isFloating)
|
||||
Positioned(
|
||||
child: Stack(
|
||||
children: [
|
||||
..._buildContent(orientation, isFloating),
|
||||
if (!isFloating)
|
||||
Positioned(
|
||||
top: 24.0,
|
||||
left: 24.0,
|
||||
child: IconButton(
|
||||
@ -599,9 +637,15 @@ class MyCallingPage extends State<Calling> {
|
||||
onPressed: () {
|
||||
PIPView.of(context)?.setFloating(true);
|
||||
},
|
||||
))
|
||||
]));
|
||||
}));
|
||||
});
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -198,9 +198,11 @@ class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
|
||||
: Tween<Offset>(
|
||||
begin: _dragOffset,
|
||||
end: calculatedOffset,
|
||||
).transform(_dragAnimationController.isAnimating
|
||||
? dragAnimationValue
|
||||
: toggleFloatingAnimationValue);
|
||||
).transform(
|
||||
_dragAnimationController.isAnimating
|
||||
? dragAnimationValue
|
||||
: toggleFloatingAnimationValue,
|
||||
);
|
||||
final borderRadius = Tween<double>(
|
||||
begin: 0,
|
||||
end: 10,
|
||||
|
@ -21,52 +21,54 @@ class HomeserverBottomSheet extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: Text(homeserver.homeserver.baseUrl.host),
|
||||
),
|
||||
body: ListView(children: [
|
||||
if (description != null && description.isNotEmpty)
|
||||
body: ListView(
|
||||
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(
|
||||
leading: const Icon(Icons.info_outlined),
|
||||
title: Text(description),
|
||||
onTap: () =>
|
||||
launchUrlString(homeserver.homeserver.baseUrl.toString()),
|
||||
leading: const Icon(Icons.link_outlined),
|
||||
title: Text(homeserver.homeserver.baseUrl.toString()),
|
||||
),
|
||||
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(
|
||||
onTap: () =>
|
||||
launchUrlString(homeserver.homeserver.baseUrl.toString()),
|
||||
leading: const Icon(Icons.link_outlined),
|
||||
title: Text(homeserver.homeserver.baseUrl.toString()),
|
||||
),
|
||||
if (registration != null)
|
||||
ListTile(
|
||||
onTap: () => launchUrlString(registration.toString()),
|
||||
leading: const Icon(Icons.person_add_outlined),
|
||||
title: Text(registration.toString()),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
]),
|
||||
if (registration != null)
|
||||
ListTile(
|
||||
onTap: () => launchUrlString(registration.toString()),
|
||||
leading: const Icon(Icons.person_add_outlined),
|
||||
title: Text(registration.toString()),
|
||||
),
|
||||
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(
|
||||
(e, s) async {
|
||||
await showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.indexedDbErrorTitle,
|
||||
message: L10n.of(context)!.indexedDbErrorLong,
|
||||
onWillPop: () async => false);
|
||||
context: context,
|
||||
title: L10n.of(context)!.indexedDbErrorTitle,
|
||||
message: L10n.of(context)!.indexedDbErrorLong,
|
||||
onWillPop: () async => false,
|
||||
);
|
||||
_checkTorBrowser();
|
||||
},
|
||||
);
|
||||
@ -85,9 +86,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
});
|
||||
|
||||
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
|
||||
.where((element) =>
|
||||
element.homeserver.baseUrl.host.contains(searchTerm) ||
|
||||
(element.homeserver.description?.contains(searchTerm) ?? false))
|
||||
.where(
|
||||
(element) =>
|
||||
element.homeserver.baseUrl.host.contains(searchTerm) ||
|
||||
(element.homeserver.description?.contains(searchTerm) ?? false),
|
||||
)
|
||||
.toList();
|
||||
|
||||
void _loadHomeserverList() async {
|
||||
@ -186,15 +189,16 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
final file = await FilePickerCross.importFromStorage();
|
||||
if (file.fileName == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
try {
|
||||
final client = Matrix.of(context).getLoginClient();
|
||||
await client.importDump(file.toString());
|
||||
Matrix.of(context).initMatrix();
|
||||
} catch (e, s) {
|
||||
Logs().e('Future error:', e, s);
|
||||
}
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
try {
|
||||
final client = Matrix.of(context).getLoginClient();
|
||||
await client.importDump(file.toString());
|
||||
Matrix.of(context).initMatrix();
|
||||
} catch (e, s) {
|
||||
Logs().e('Future error:', e, s);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -65,10 +65,11 @@ class HomeserverPickerView extends StatelessWidget {
|
||||
child: benchmarkResults == null
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child:
|
||||
CircularProgressIndicator.adaptive(),
|
||||
))
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: CircularProgressIndicator
|
||||
.adaptive(),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: controller.filteredHomeservers
|
||||
.map(
|
||||
@ -82,19 +83,20 @@ class HomeserverPickerView extends StatelessWidget {
|
||||
.showServerInfo(server),
|
||||
),
|
||||
onTap: () => controller.setServer(
|
||||
server
|
||||
.homeserver.baseUrl.host),
|
||||
server.homeserver.baseUrl.host,
|
||||
),
|
||||
title: Text(
|
||||
server.homeserver.baseUrl.host,
|
||||
style: const TextStyle(
|
||||
color: Colors.black),
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
server.homeserver.description ??
|
||||
'',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Colors.grey.shade700),
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -42,12 +42,13 @@ class ImageViewerView extends StatelessWidget {
|
||||
if (PlatformInfos.isMobile)
|
||||
// Use builder context to correctly position the share dialog on iPad
|
||||
Builder(
|
||||
builder: (context) => IconButton(
|
||||
onPressed: () => controller.shareFileAction(context),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
))
|
||||
builder: (context) => IconButton(
|
||||
onPressed: () => controller.shareFileAction(context),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: InteractiveViewer(
|
||||
|
@ -71,8 +71,11 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
||||
future: () => room.invite(id),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +102,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
||||
response = await matrix.client.searchUserDirectory(text, limit: 10);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text((e).toLocalizedString(context))));
|
||||
SnackBar(content: Text((e).toLocalizedString(context))),
|
||||
);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
@ -108,19 +112,25 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
||||
foundProfiles = List<Profile>.from(response.results);
|
||||
if (text.isValidMatrixId &&
|
||||
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
|
||||
setState(() => foundProfiles = [
|
||||
Profile.fromJson({'user_id': text}),
|
||||
]);
|
||||
setState(
|
||||
() => foundProfiles = [
|
||||
Profile.fromJson({'user_id': text}),
|
||||
],
|
||||
);
|
||||
}
|
||||
final participants = Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(roomId!)!
|
||||
.getParticipants()
|
||||
.where((user) =>
|
||||
[Membership.join, Membership.invite].contains(user.membership))
|
||||
.where(
|
||||
(user) =>
|
||||
[Membership.join, Membership.invite].contains(user.membership),
|
||||
)
|
||||
.toList();
|
||||
foundProfiles.removeWhere((profile) =>
|
||||
participants.indexWhere((u) => u.id == profile.userId) != -1);
|
||||
foundProfiles.removeWhere(
|
||||
(profile) =>
|
||||
participants.indexWhere((u) => u.id == profile.userId) != -1,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,9 @@ class InvitationSelectionView extends StatelessWidget {
|
||||
),
|
||||
subtitle: Text(controller.foundProfiles[i].userId),
|
||||
onTap: () => controller.inviteAction(
|
||||
context, controller.foundProfiles[i].userId),
|
||||
context,
|
||||
controller.foundProfiles[i].userId,
|
||||
),
|
||||
),
|
||||
)
|
||||
: FutureBuilder<List<User>>(
|
||||
@ -106,7 +108,8 @@ class InvitationSelectionView extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
onTap: () =>
|
||||
controller.inviteAction(context, contacts[i].id),
|
||||
|
@ -70,19 +70,20 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
if (input.isEmpty) return;
|
||||
|
||||
final valid = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
// make sure the loading spinner shows before we test the keys
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
var valid = false;
|
||||
try {
|
||||
await widget.request.openSSSS(keyOrPassphrase: input);
|
||||
valid = true;
|
||||
} catch (_) {
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
// make sure the loading spinner shows before we test the keys
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
var valid = false;
|
||||
try {
|
||||
await widget.request.openSSSS(keyOrPassphrase: input);
|
||||
valid = true;
|
||||
} catch (_) {
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
},
|
||||
);
|
||||
if (valid.error != null) {
|
||||
await showOkAlertDialog(
|
||||
useRootNavigator: false,
|
||||
@ -117,8 +118,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context)!.askSSSSSign,
|
||||
style: const TextStyle(fontSize: 20)),
|
||||
Text(
|
||||
L10n.of(context)!.askSSSSSign,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
Container(height: 10),
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
@ -141,18 +144,22 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
],
|
||||
),
|
||||
);
|
||||
buttons.add(TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.submit,
|
||||
buttons.add(
|
||||
TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.submit,
|
||||
),
|
||||
onPressed: () => checkInput(textEditingController.text),
|
||||
),
|
||||
onPressed: () => checkInput(textEditingController.text),
|
||||
));
|
||||
buttons.add(TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.skip,
|
||||
);
|
||||
buttons.add(
|
||||
TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.skip,
|
||||
),
|
||||
onPressed: () => widget.request.openSSSS(skip: true),
|
||||
),
|
||||
onPressed: () => widget.request.openSSSS(skip: true),
|
||||
));
|
||||
);
|
||||
break;
|
||||
case KeyVerificationState.askAccept:
|
||||
title = Text(L10n.of(context)!.newVerificationRequest);
|
||||
@ -171,19 +178,23 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
)
|
||||
],
|
||||
);
|
||||
buttons.add(TextButton.icon(
|
||||
icon: const Icon(Icons.close),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
label: Text(L10n.of(context)!.reject),
|
||||
onPressed: () => widget.request
|
||||
.rejectVerification()
|
||||
.then((_) => Navigator.of(context, rootNavigator: false).pop()),
|
||||
));
|
||||
buttons.add(TextButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(L10n.of(context)!.accept),
|
||||
onPressed: () => widget.request.acceptVerification(),
|
||||
));
|
||||
buttons.add(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.close),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
label: Text(L10n.of(context)!.reject),
|
||||
onPressed: () => widget.request
|
||||
.rejectVerification()
|
||||
.then((_) => Navigator.of(context, rootNavigator: false).pop()),
|
||||
),
|
||||
);
|
||||
buttons.add(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(L10n.of(context)!.accept),
|
||||
onPressed: () => widget.request.acceptVerification(),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case KeyVerificationState.waitingAccept:
|
||||
body = Center(
|
||||
@ -245,19 +256,23 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
),
|
||||
],
|
||||
);
|
||||
buttons.add(TextButton.icon(
|
||||
icon: const Icon(Icons.close),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
buttons.add(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.close),
|
||||
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(
|
||||
icon: const Icon(Icons.check_outlined),
|
||||
label: Text(L10n.of(context)!.theyMatch),
|
||||
onPressed: () => widget.request.acceptSas(),
|
||||
));
|
||||
);
|
||||
buttons.add(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.check_outlined),
|
||||
label: Text(L10n.of(context)!.theyMatch),
|
||||
onPressed: () => widget.request.acceptSas(),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case KeyVerificationState.waitingSas:
|
||||
final acceptText = widget.request.sasTypes.contains('emoji')
|
||||
@ -279,8 +294,11 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
body = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(Icons.check_circle_outlined,
|
||||
color: Colors.green, size: 128.0),
|
||||
const Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: Colors.green,
|
||||
size: 128.0,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
L10n.of(context)!.verifySuccess,
|
||||
@ -288,12 +306,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
),
|
||||
],
|
||||
);
|
||||
buttons.add(TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.close,
|
||||
buttons.add(
|
||||
TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.close,
|
||||
),
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
),
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
));
|
||||
);
|
||||
break;
|
||||
case KeyVerificationState.error:
|
||||
body = Column(
|
||||
@ -307,12 +327,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
|
||||
),
|
||||
],
|
||||
);
|
||||
buttons.add(TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.close,
|
||||
buttons.add(
|
||||
TextButton(
|
||||
child: Text(
|
||||
L10n.of(context)!.close,
|
||||
),
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
),
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
));
|
||||
);
|
||||
break;
|
||||
}
|
||||
return Scaffold(
|
||||
@ -350,7 +372,8 @@ class _Emoji extends StatelessWidget {
|
||||
return emoji.name;
|
||||
}
|
||||
final translations = Map<String, String?>.from(
|
||||
sasEmoji[emoji.number]['translated_descriptions']);
|
||||
sasEmoji[emoji.number]['translated_descriptions'],
|
||||
);
|
||||
translations['en'] = emoji.name;
|
||||
for (final locale in window.locales) {
|
||||
final wantLocaleParts = locale.toString().split('_');
|
||||
|
@ -67,15 +67,17 @@ class LoginController extends State<Login> {
|
||||
} else {
|
||||
identifier = AuthenticationUserIdentifier(user: username);
|
||||
}
|
||||
await matrix.getLoginClient().login(LoginType.mLoginPassword,
|
||||
identifier: identifier,
|
||||
// To stay compatible with older server versions
|
||||
// ignore: deprecated_member_use
|
||||
user: identifier.type == AuthenticationIdentifierTypes.userId
|
||||
? username
|
||||
: null,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName);
|
||||
await matrix.getLoginClient().login(
|
||||
LoginType.mLoginPassword,
|
||||
identifier: identifier,
|
||||
// To stay compatible with older server versions
|
||||
// ignore: deprecated_member_use
|
||||
user: identifier.type == AuthenticationIdentifierTypes.userId
|
||||
? username
|
||||
: null,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
);
|
||||
} on MatrixException catch (exception) {
|
||||
setState(() => passwordError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
@ -121,7 +123,8 @@ class LoginController extends State<Login> {
|
||||
Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
|
||||
// okay, the server we checked does not appear to be a matrix server
|
||||
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(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
@ -230,7 +233,8 @@ class LoginController extends State<Login> {
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)));
|
||||
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)),
|
||||
);
|
||||
usernameController.text = input.single;
|
||||
passwordController.text = password.single;
|
||||
login();
|
||||
|
@ -19,125 +19,131 @@ class LoginView extends StatelessWidget {
|
||||
automaticallyImplyLeading: !controller.loading,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
L10n.of(context)!.logInTo(Matrix.of(context)
|
||||
.getLoginClient()
|
||||
.homeserver
|
||||
.toString()
|
||||
.replaceFirst('https://', '')),
|
||||
L10n.of(context)!.logInTo(
|
||||
Matrix.of(context)
|
||||
.getLoginClient()
|
||||
.homeserver
|
||||
.toString()
|
||||
.replaceFirst('https://', ''),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Builder(builder: (context) {
|
||||
return AutofillGroup(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
onChanged: controller.checkWellKnownWithCoolDown,
|
||||
controller: controller.usernameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
||||
errorText: controller.usernameError,
|
||||
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,
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return AutofillGroup(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
readOnly: controller.loading,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
onChanged: controller.checkWellKnownWithCoolDown,
|
||||
controller: controller.usernameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
||||
errorText: controller.usernameError,
|
||||
errorStyle: const TextStyle(color: Colors.orange),
|
||||
hintText: L10n.of(context)!.emailOrUsername,
|
||||
),
|
||||
hintText: L10n.of(context)!.password,
|
||||
),
|
||||
),
|
||||
),
|
||||
Hero(
|
||||
tag: 'signinButton',
|
||||
child: Padding(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.loading
|
||||
? () {}
|
||||
: controller.passwordForgotten,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
backgroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
onPressed: controller.loading ? null : controller.login,
|
||||
icon: const Icon(Icons.login_outlined),
|
||||
label: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.login),
|
||||
icon: const Icon(Icons.safety_check_outlined),
|
||||
label: Text(L10n.of(context)!.passwordForgotten),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
onSubmitted: controller.submitAction,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context)!.optionalGroupName,
|
||||
prefixIcon: const Icon(Icons.people_outlined),
|
||||
hintText: L10n.of(context)!.enterAGroupName),
|
||||
labelText: L10n.of(context)!.optionalGroupName,
|
||||
prefixIcon: const Icon(Icons.people_outlined),
|
||||
hintText: L10n.of(context)!.enterAGroupName,
|
||||
),
|
||||
),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
|
@ -29,9 +29,10 @@ class NewSpaceView extends StatelessWidget {
|
||||
textInputAction: TextInputAction.go,
|
||||
onSubmitted: controller.submitAction,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context)!.spaceName,
|
||||
prefixIcon: const Icon(Icons.people_outlined),
|
||||
hintText: L10n.of(context)!.enterASpacepName),
|
||||
labelText: L10n.of(context)!.spaceName,
|
||||
prefixIcon: const Icon(Icons.people_outlined),
|
||||
hintText: L10n.of(context)!.enterASpacepName,
|
||||
),
|
||||
),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
|
@ -40,98 +40,100 @@ class SettingsView extends StatelessWidget {
|
||||
key: const Key('SettingsListViewContent'),
|
||||
children: <Widget>[
|
||||
FutureBuilder<Profile>(
|
||||
future: controller.profileFuture,
|
||||
builder: (context, snapshot) {
|
||||
final profile = snapshot.data;
|
||||
final mxid = Matrix.of(context).client.userID ??
|
||||
L10n.of(context)!.user;
|
||||
final displayname =
|
||||
profile?.displayName ?? mxid.localpart ?? mxid;
|
||||
return Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
elevation: Theme.of(context)
|
||||
.appBarTheme
|
||||
.scrolledUnderElevation ??
|
||||
4,
|
||||
shadowColor:
|
||||
Theme.of(context).appBarTheme.shadowColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Avatar.defaultSize * 2.5),
|
||||
future: controller.profileFuture,
|
||||
builder: (context, snapshot) {
|
||||
final profile = snapshot.data;
|
||||
final mxid =
|
||||
Matrix.of(context).client.userID ?? L10n.of(context)!.user;
|
||||
final displayname =
|
||||
profile?.displayName ?? mxid.localpart ?? mxid;
|
||||
return Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Material(
|
||||
elevation: Theme.of(context)
|
||||
.appBarTheme
|
||||
.scrolledUnderElevation ??
|
||||
4,
|
||||
shadowColor:
|
||||
Theme.of(context).appBarTheme.shadowColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
child: Avatar(
|
||||
mxContent: profile?.avatarUrl,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
fontSize: 18 * 2.5,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Avatar.defaultSize * 2.5,
|
||||
),
|
||||
),
|
||||
if (profile != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: controller.setAvatarAction,
|
||||
heroTag: null,
|
||||
child: const Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
child: Avatar(
|
||||
mxContent: profile?.avatarUrl,
|
||||
name: displayname,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
fontSize: 18 * 2.5,
|
||||
),
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: controller.setDisplaynameAction,
|
||||
icon: const Icon(
|
||||
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),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: controller.setDisplaynameAction,
|
||||
icon: const Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 16,
|
||||
),
|
||||
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),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
if (showChatBackupBanner == null)
|
||||
ListTile(
|
||||
|
@ -79,11 +79,12 @@ class Settings3PidController extends State<Settings3Pid> {
|
||||
return;
|
||||
}
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.delete3pidFromAccount(
|
||||
identifier.address,
|
||||
identifier.medium,
|
||||
));
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.delete3pidFromAccount(
|
||||
identifier.address,
|
||||
identifier.medium,
|
||||
),
|
||||
);
|
||||
if (success.error != null) return;
|
||||
setState(() => request = null);
|
||||
}
|
||||
|
@ -30,8 +30,10 @@ class Settings3PidView extends StatelessWidget {
|
||||
body: MaxWidthBody(
|
||||
child: FutureBuilder<List<ThirdPartyIdentifier>?>(
|
||||
future: controller.request,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot) {
|
||||
builder: (
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot,
|
||||
) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
@ -42,7 +44,8 @@ class Settings3PidView extends StatelessWidget {
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
);
|
||||
}
|
||||
final identifier = snapshot.data!;
|
||||
return Column(
|
||||
@ -71,10 +74,11 @@ class Settings3PidView extends StatelessWidget {
|
||||
itemCount: identifier.length,
|
||||
itemBuilder: (BuildContext context, int i) => ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(identifier[i].iconData)),
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: Colors.grey,
|
||||
child: Icon(identifier[i].iconData),
|
||||
),
|
||||
title: Text(identifier[i].address),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context)!.delete,
|
||||
|
@ -60,13 +60,20 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => client.setRoomStateWithKey(
|
||||
room!.id, 'im.ponies.room_emotes', stateKey ?? '', pack!.toJson()),
|
||||
room!.id,
|
||||
'im.ponies.room_emotes',
|
||||
stateKey ?? '',
|
||||
pack!.toJson(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
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(
|
||||
context: context,
|
||||
future: () => client.setAccountData(
|
||||
client.userID!, 'im.ponies.emote_rooms', content),
|
||||
client.userID!,
|
||||
'im.ponies.emote_rooms',
|
||||
content,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
@ -197,7 +207,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
|
||||
}
|
||||
|
||||
void imagePickerAction(
|
||||
ValueNotifier<ImagePackImageContent?> controller) async {
|
||||
ValueNotifier<ImagePackImageContent?> controller,
|
||||
) async {
|
||||
final result =
|
||||
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
|
||||
if (result.fileName == null) return;
|
||||
|
@ -140,16 +140,17 @@ class EmotesSettingsView extends StatelessWidget {
|
||||
actions: !useShortCuts
|
||||
? {}
|
||||
: {
|
||||
SubmitLineIntent:
|
||||
CallbackAction(onInvoke: (i) {
|
||||
controller.submitImageAction(
|
||||
imageCode,
|
||||
textEditingController.text,
|
||||
image,
|
||||
textEditingController,
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
SubmitLineIntent: CallbackAction(
|
||||
onInvoke: (i) {
|
||||
controller.submitImageAction(
|
||||
imageCode,
|
||||
textEditingController.text,
|
||||
image,
|
||||
textEditingController,
|
||||
);
|
||||
return null;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: TextField(
|
||||
readOnly: controller.readonly,
|
||||
|
@ -58,34 +58,36 @@ class SettingsIgnoreListView extends StatelessWidget {
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: StreamBuilder<Object>(
|
||||
stream: client.onAccountData.stream
|
||||
.where((a) => a.type == 'm.ignored_user_list'),
|
||||
builder: (context, snapshot) {
|
||||
return ListView.builder(
|
||||
itemCount: client.ignoredUsers.length,
|
||||
itemBuilder: (c, i) => FutureBuilder<Profile>(
|
||||
future:
|
||||
client.getProfileFromUserId(client.ignoredUsers[i]),
|
||||
builder: (c, s) => ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: s.data?.avatarUrl ?? Uri.parse(''),
|
||||
name: s.data?.displayName ?? client.ignoredUsers[i],
|
||||
),
|
||||
title: Text(
|
||||
s.data?.displayName ?? client.ignoredUsers[i]),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context)!.delete,
|
||||
icon: const Icon(Icons.delete_forever_outlined),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
client.unignoreUser(client.ignoredUsers[i]),
|
||||
),
|
||||
stream: client.onAccountData.stream
|
||||
.where((a) => a.type == 'm.ignored_user_list'),
|
||||
builder: (context, snapshot) {
|
||||
return ListView.builder(
|
||||
itemCount: client.ignoredUsers.length,
|
||||
itemBuilder: (c, i) => FutureBuilder<Profile>(
|
||||
future:
|
||||
client.getProfileFromUserId(client.ignoredUsers[i]),
|
||||
builder: (c, s) => ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: s.data?.avatarUrl ?? Uri.parse(''),
|
||||
name: s.data?.displayName ?? client.ignoredUsers[i],
|
||||
),
|
||||
title: Text(
|
||||
s.data?.displayName ?? client.ignoredUsers[i],
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: L10n.of(context)!.delete,
|
||||
icon: const Icon(Icons.delete_forever_outlined),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
client.unignoreUser(client.ignoredUsers[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -32,27 +32,28 @@ class MultipleEmotesSettingsView extends StatelessWidget {
|
||||
final keys = packs.keys.toList();
|
||||
keys.sort();
|
||||
return ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) => Container(),
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final event = packs[keys[i]];
|
||||
String? packName =
|
||||
keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
||||
if (event != null && event.content['pack'] is Map) {
|
||||
if (event.content['pack']['displayname'] is String) {
|
||||
packName = event.content['pack']['displayname'];
|
||||
} else if (event.content['pack']['name'] is String) {
|
||||
packName = event.content['pack']['name'];
|
||||
}
|
||||
separatorBuilder: (BuildContext context, int i) => Container(),
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final event = packs[keys[i]];
|
||||
String? packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
|
||||
if (event != null && event.content['pack'] is Map) {
|
||||
if (event.content['pack']['displayname'] is String) {
|
||||
packName = event.content['pack']['displayname'];
|
||||
} else if (event.content['pack']['name'] is String) {
|
||||
packName = event.content['pack']['name'];
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(packName!),
|
||||
onTap: () async {
|
||||
VRouter.of(context).toSegments(
|
||||
['rooms', room.id, 'details', 'emotes', keys[i]]);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(packName!),
|
||||
onTap: () async {
|
||||
VRouter.of(context).toSegments(
|
||||
['rooms', room.id, 'details', 'emotes', keys[i]],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -25,90 +25,94 @@ class SettingsNotificationsView extends StatelessWidget {
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: StreamBuilder(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onAccountData
|
||||
.stream
|
||||
.where((event) => event.type == 'm.push_rules'),
|
||||
builder: (BuildContext context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
value: !Matrix.of(context).client.allPushNotificationsMuted,
|
||||
title: Text(
|
||||
L10n.of(context)!.notificationsEnabledForThisAccount),
|
||||
onChanged: (_) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).client.setMuteAllPushNotifications(
|
||||
!Matrix.of(context)
|
||||
.client
|
||||
.allPushNotificationsMuted,
|
||||
),
|
||||
),
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onAccountData
|
||||
.stream
|
||||
.where((event) => event.type == 'm.push_rules'),
|
||||
builder: (BuildContext context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
value: !Matrix.of(context).client.allPushNotificationsMuted,
|
||||
title: Text(
|
||||
L10n.of(context)!.notificationsEnabledForThisAccount,
|
||||
),
|
||||
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
|
||||
const Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.pushRules,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
onChanged: (_) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.setMuteAllPushNotifications(
|
||||
!Matrix.of(context).client.allPushNotificationsMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (var item in NotificationSettingsItem.items)
|
||||
SwitchListTile.adaptive(
|
||||
value: controller.getNotificationSetting(item) ?? true,
|
||||
title: Text(item.title(context)),
|
||||
onChanged: (bool enabled) =>
|
||||
controller.setNotificationSetting(item, enabled),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
|
||||
const Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.devices,
|
||||
L10n.of(context)!.pushRules,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<List<Pusher>?>(
|
||||
future: controller.pusherFuture ??=
|
||||
Matrix.of(context).client.getPushers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
Center(
|
||||
child: Text(
|
||||
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]),
|
||||
for (var item in NotificationSettingsItem.items)
|
||||
SwitchListTile.adaptive(
|
||||
value: controller.getNotificationSetting(item) ?? true,
|
||||
title: Text(item.title(context)),
|
||||
onChanged: (bool enabled) =>
|
||||
controller.setNotificationSetting(item, enabled),
|
||||
),
|
||||
},
|
||||
const Divider(thickness: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.devices,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<List<Pusher>?>(
|
||||
future: controller.pusherFuture ??=
|
||||
Matrix.of(context).client.getPushers(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
Center(
|
||||
child: Text(
|
||||
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) {
|
||||
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(
|
||||
password: input.single,
|
||||
identifier: AuthenticationUserIdentifier(
|
||||
user: Matrix.of(context).client.userID!),
|
||||
user: Matrix.of(context).client.userID!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -181,10 +183,11 @@ class SettingsSecurityController extends State<SettingsSecurity> {
|
||||
try {
|
||||
final export = await Matrix.of(context).client.exportDump();
|
||||
final filePickerCross = FilePickerCross(
|
||||
Uint8List.fromList(const Utf8Codec().encode(export!)),
|
||||
path:
|
||||
'/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup',
|
||||
fileExtension: 'fluffybackup');
|
||||
Uint8List.fromList(const Utf8Codec().encode(export!)),
|
||||
path:
|
||||
'/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup',
|
||||
fileExtension: 'fluffybackup',
|
||||
);
|
||||
await filePickerCross.exportToStorage(
|
||||
subject: L10n.of(context)!.dehydrateShare,
|
||||
);
|
||||
|
@ -32,14 +32,15 @@ class SettingsStoriesController extends State<SettingsStories> {
|
||||
final blockList = room.client.storiesBlockList;
|
||||
blockList.add(user.id);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await user.kick();
|
||||
await room.client.setStoriesBlockList(blockList.toSet().toList());
|
||||
setState(() {
|
||||
users[user] = false;
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
await user.kick();
|
||||
await room.client.setStoriesBlockList(blockList.toSet().toList());
|
||||
setState(() {
|
||||
users[user] = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -47,14 +48,15 @@ class SettingsStoriesController extends State<SettingsStories> {
|
||||
final blockList = room.client.storiesBlockList;
|
||||
blockList.remove(user.id);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await room.client.setStoriesBlockList(blockList);
|
||||
await room.invite(user.id);
|
||||
setState(() {
|
||||
users[user] = true;
|
||||
});
|
||||
context: context,
|
||||
future: () async {
|
||||
await room.client.setStoriesBlockList(blockList);
|
||||
await room.invite(user.id);
|
||||
setState(() {
|
||||
users[user] = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -38,9 +38,10 @@ class SettingsStoriesView extends StatelessWidget {
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
));
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.users.length,
|
||||
|
@ -59,16 +59,18 @@ class SettingsStyleView extends StatelessWidget {
|
||||
borderRadius:
|
||||
BorderRadius.circular(colorPickerSize),
|
||||
child: SizedBox(
|
||||
width: colorPickerSize,
|
||||
height: colorPickerSize,
|
||||
child: controller.currentColor == color
|
||||
? const Center(
|
||||
child: Icon(
|
||||
width: colorPickerSize,
|
||||
height: colorPickerSize,
|
||||
child: controller.currentColor == color
|
||||
? const Center(
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
))
|
||||
: null),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -118,16 +120,18 @@ class SettingsStyleView extends StatelessWidget {
|
||||
),
|
||||
onTap: controller.deleteWallpaperAction,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context)!.changeWallpaper),
|
||||
trailing: Icon(
|
||||
Icons.photo_outlined,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
onTap: controller.setWallpaperAction,
|
||||
);
|
||||
}),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return ListTile(
|
||||
title: Text(L10n.of(context)!.changeWallpaper),
|
||||
trailing: Icon(
|
||||
Icons.photo_outlined,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
onTap: controller.setWallpaperAction,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
|
@ -46,10 +46,11 @@ class StoryPageController extends State<StoryPage> {
|
||||
Timeline? timeline;
|
||||
|
||||
Event? get currentEvent => index < events.length ? events[index] : null;
|
||||
StoryThemeData get storyThemeData =>
|
||||
StoryThemeData.fromJson(currentEvent?.content
|
||||
.tryGetMap<String, dynamic>(StoryThemeData.contentKey) ??
|
||||
{});
|
||||
StoryThemeData get storyThemeData => StoryThemeData.fromJson(
|
||||
currentEvent?.content
|
||||
.tryGetMap<String, dynamic>(StoryThemeData.contentKey) ??
|
||||
{},
|
||||
);
|
||||
|
||||
bool replyLoading = false;
|
||||
bool _modalOpened = false;
|
||||
@ -83,8 +84,9 @@ class StoryPageController extends State<StoryPage> {
|
||||
final client = Matrix.of(context).client;
|
||||
final roomId = await client.startDirectChat(currentEvent.senderId);
|
||||
var replyText = L10n.of(context)!.storyFrom(
|
||||
currentEvent.originServerTs.localizedTime(context),
|
||||
currentEvent.content.tryGet<String>('body') ?? '');
|
||||
currentEvent.originServerTs.localizedTime(context),
|
||||
currentEvent.content.tryGet<String>('body') ?? '',
|
||||
);
|
||||
replyText = replyText.split('\n').map((line) => '> $line').join('\n');
|
||||
message = '$replyText\n\n$message';
|
||||
await client.getRoomById(roomId)!.sendTextEvent(message);
|
||||
@ -307,33 +309,35 @@ class StoryPageController extends State<StoryPage> {
|
||||
final event = currentEvent;
|
||||
if (event == null) return;
|
||||
final score = await showConfirmationDialog<int>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.reportMessage,
|
||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context)!.extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context)!.offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context)!.inoffensive,
|
||||
),
|
||||
]);
|
||||
context: context,
|
||||
title: L10n.of(context)!.reportMessage,
|
||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context)!.extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context)!.offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context)!.inoffensive,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (score == null) return;
|
||||
final reason = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||
);
|
||||
if (reason == null || reason.single.isEmpty) return;
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
@ -352,7 +356,9 @@ class StoryPageController extends State<StoryPage> {
|
||||
}
|
||||
|
||||
Future<MatrixFile> downloadAndDecryptAttachment(
|
||||
Event event, bool getThumbnail) async {
|
||||
Event event,
|
||||
bool getThumbnail,
|
||||
) async {
|
||||
return _fileCache[event.eventId] ??=
|
||||
event.downloadAndDecryptAttachment(getThumbnail: getThumbnail);
|
||||
}
|
||||
@ -400,10 +406,12 @@ class StoryPageController extends State<StoryPage> {
|
||||
final timeline = this.timeline = await room.getTimeline();
|
||||
timeline.requestKeys();
|
||||
var events = timeline.events
|
||||
.where((e) =>
|
||||
e.type == EventTypes.Message &&
|
||||
!e.redacted &&
|
||||
e.status == EventStatus.synced)
|
||||
.where(
|
||||
(e) =>
|
||||
e.type == EventTypes.Message &&
|
||||
!e.redacted &&
|
||||
e.status == EventStatus.synced,
|
||||
)
|
||||
.toList();
|
||||
|
||||
final hasOutdatedEvents = events.removeOutdatedEvents();
|
||||
@ -432,12 +440,16 @@ class StoryPageController extends State<StoryPage> {
|
||||
|
||||
// Preload images and videos
|
||||
events
|
||||
.where((event) => {MessageTypes.Image, MessageTypes.Video}
|
||||
.contains(event.messageType))
|
||||
.forEach((event) => downloadAndDecryptAttachment(
|
||||
.where(
|
||||
(event) => {MessageTypes.Image, MessageTypes.Video}
|
||||
.contains(event.messageType),
|
||||
)
|
||||
.forEach(
|
||||
(event) => downloadAndDecryptAttachment(
|
||||
event,
|
||||
event.messageType == MessageTypes.Video &&
|
||||
PlatformInfos.isMobile));
|
||||
event.messageType == MessageTypes.Video && PlatformInfos.isMobile,
|
||||
),
|
||||
);
|
||||
|
||||
// Reverse list
|
||||
this.events.clear();
|
||||
@ -502,9 +514,11 @@ class StoryPageController extends State<StoryPage> {
|
||||
|
||||
extension on List<Event> {
|
||||
bool removeOutdatedEvents() {
|
||||
final outdatedIndex = indexWhere((event) =>
|
||||
DateTime.now().difference(event.originServerTs).inHours >
|
||||
ClientStoriesExtension.lifeTimeInHours);
|
||||
final outdatedIndex = indexWhere(
|
||||
(event) =>
|
||||
DateTime.now().difference(event.originServerTs).inHours >
|
||||
ClientStoriesExtension.lifeTimeInHours,
|
||||
);
|
||||
if (outdatedIndex != -1) {
|
||||
removeRange(outdatedIndex, length);
|
||||
return true;
|
||||
|
@ -144,9 +144,10 @@ class StoryView extends StatelessWidget {
|
||||
final events = controller.events;
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
));
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (events.isEmpty) {
|
||||
return Center(
|
||||
@ -218,7 +219,9 @@ class StoryView extends StatelessWidget {
|
||||
!PlatformInfos.isMobile))
|
||||
FutureBuilder<MatrixFile>(
|
||||
future: controller.downloadAndDecryptAttachment(
|
||||
event, event.messageType == MessageTypes.Video),
|
||||
event,
|
||||
event.messageType == MessageTypes.Video,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final matrixFile = snapshot.data;
|
||||
if (matrixFile == null) {
|
||||
@ -364,7 +367,8 @@ class StoryView extends StatelessWidget {
|
||||
height: 16,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
|
@ -52,33 +52,35 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
||||
case UserBottomSheetAction.report:
|
||||
final event = widget.user;
|
||||
final score = await showConfirmationDialog<int>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.reportUser,
|
||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context)!.extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context)!.offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context)!.inoffensive,
|
||||
),
|
||||
]);
|
||||
context: context,
|
||||
title: L10n.of(context)!.reportUser,
|
||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
actions: [
|
||||
AlertDialogAction(
|
||||
key: -100,
|
||||
label: L10n.of(context)!.extremeOffensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: -50,
|
||||
label: L10n.of(context)!.offensive,
|
||||
),
|
||||
AlertDialogAction(
|
||||
key: 0,
|
||||
label: L10n.of(context)!.inoffensive,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (score == null) return;
|
||||
final reason = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||
);
|
||||
if (reason == null || reason.single.isEmpty) return;
|
||||
final result = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
@ -91,7 +93,8 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
||||
);
|
||||
if (result.error != null) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)));
|
||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
|
||||
);
|
||||
break;
|
||||
case UserBottomSheetAction.mention:
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
@ -151,9 +154,9 @@ class UserBottomSheetController extends State<UserBottomSheet> {
|
||||
case UserBottomSheetAction.ignore:
|
||||
if (await askConfirmation()) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
Matrix.of(context).client.ignoreUser(widget.user.id));
|
||||
context: context,
|
||||
future: () => Matrix.of(context).client.ignoreUser(widget.user.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,10 +55,12 @@ extension AccountBundlesExtension on Client {
|
||||
}
|
||||
ret ??= [];
|
||||
if (ret.isEmpty) {
|
||||
ret.add(AccountBundle(
|
||||
name: userID,
|
||||
priority: 0,
|
||||
));
|
||||
ret.add(
|
||||
AccountBundle(
|
||||
name: userID,
|
||||
priority: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
@ -78,7 +78,8 @@ class BackgroundPush {
|
||||
firebase?.setListeners(
|
||||
onMessage: (message) => pushHelper(
|
||||
PushNotification.fromJson(
|
||||
Map<String, dynamic>.from(message['data'] ?? message)),
|
||||
Map<String, dynamic>.from(message['data'] ?? message),
|
||||
),
|
||||
client: client,
|
||||
l10n: l10n,
|
||||
activeRoomId: router?.currentState?.pathParameters['roomid'],
|
||||
@ -331,7 +332,8 @@ class BackgroundPush {
|
||||
}
|
||||
} catch (e) {
|
||||
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');
|
||||
final oldTokens = <String?>{};
|
||||
@ -366,7 +368,8 @@ class BackgroundPush {
|
||||
Future<void> _onUpMessage(Uint8List message, String i) async {
|
||||
upAction = true;
|
||||
final data = Map<String, dynamic>.from(
|
||||
json.decode(utf8.decode(message))['notification']);
|
||||
json.decode(utf8.decode(message))['notification'],
|
||||
);
|
||||
// UP may strip the devices list
|
||||
data['devices'] ??= [];
|
||||
await pushHelper(
|
||||
@ -382,8 +385,11 @@ class BackgroundPush {
|
||||
/// IDs we map the [roomId] to a number and store this number.
|
||||
late Map<String, int> idMap;
|
||||
Future<void> _loadIdMap() async {
|
||||
idMap = Map<String, int>.from(json.decode(
|
||||
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}'));
|
||||
idMap = Map<String, int>.from(
|
||||
json.decode(
|
||||
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> mapRoomIdToInt(String roomId) async {
|
||||
@ -441,7 +447,8 @@ class BackgroundPush {
|
||||
if (syncErrored) {
|
||||
try {
|
||||
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 notificationRooms =
|
||||
notifications.notifications.map((n) => n.roomId).toSet();
|
||||
@ -450,8 +457,9 @@ class BackgroundPush {
|
||||
.map((r) => r.id);
|
||||
} catch (e) {
|
||||
Logs().v(
|
||||
'[Push] failed to fetch pending notifications for clearing push, falling back...',
|
||||
e);
|
||||
'[Push] failed to fetch pending notifications for clearing push, falling back...',
|
||||
e,
|
||||
);
|
||||
emptyRooms = client.rooms
|
||||
.where((r) => r.notificationCount == 0)
|
||||
.map((r) => r.id);
|
||||
@ -474,7 +482,9 @@ class BackgroundPush {
|
||||
}
|
||||
if (changed) {
|
||||
await store.setItem(
|
||||
SettingKeys.notificationCurrentIds, json.encode(idMap));
|
||||
SettingKeys.notificationCurrentIds,
|
||||
json.encode(idMap),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_clearingPushLock = false;
|
||||
|
@ -39,19 +39,25 @@ abstract class ClientManager {
|
||||
}
|
||||
final clients = clientNames.map(createClient).toList();
|
||||
if (initialize) {
|
||||
await Future.wait(clients.map((client) => client
|
||||
.init(
|
||||
waitForFirstSync: false,
|
||||
waitUntilLoadCompletedLoaded: false,
|
||||
)
|
||||
.catchError(
|
||||
(e, s) => Logs().e('Unable to initialize client', e, s))));
|
||||
await Future.wait(
|
||||
clients.map(
|
||||
(client) => client
|
||||
.init(
|
||||
waitForFirstSync: false,
|
||||
waitUntilLoadCompletedLoaded: false,
|
||||
)
|
||||
.catchError(
|
||||
(e, s) => Logs().e('Unable to initialize client', e, s),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (clients.length > 1 && clients.any((c) => !c.isLogged())) {
|
||||
final loggedOutClients = clients.where((c) => !c.isLogged()).toList();
|
||||
for (final client in loggedOutClients) {
|
||||
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);
|
||||
clients.remove(client);
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import 'package:matrix/matrix.dart';
|
||||
import 'package:native_imaging/native_imaging.dart' as native;
|
||||
|
||||
Future<MatrixImageFileResizedResponse?> customImageResizer(
|
||||
MatrixImageFileResizeArguments arguments) async {
|
||||
MatrixImageFileResizeArguments arguments,
|
||||
) async {
|
||||
await native.init();
|
||||
late native.Image nativeImg;
|
||||
|
||||
@ -21,7 +22,10 @@ Future<MatrixImageFileResizedResponse?> customImageResizer(
|
||||
return null;
|
||||
}
|
||||
final rgba = Uint8List.view(
|
||||
rgbaData.buffer, rgbaData.offsetInBytes, rgbaData.lengthInBytes);
|
||||
rgbaData.buffer,
|
||||
rgbaData.offsetInBytes,
|
||||
rgbaData.lengthInBytes,
|
||||
);
|
||||
|
||||
final width = dartFrame.image.width;
|
||||
final height = dartFrame.image.height;
|
||||
|
@ -77,10 +77,15 @@ extension DateTimeExtension on DateTime {
|
||||
}
|
||||
} else if (sameYear) {
|
||||
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(),
|
||||
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0'));
|
||||
return L10n.of(context)!.dateWithYear(
|
||||
year.toString(),
|
||||
month.toString().padLeft(2, '0'),
|
||||
day.toString().padLeft(2, '0'),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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();
|
||||
|
@ -19,7 +19,8 @@ abstract class FluffyShare {
|
||||
ClipboardData(text: text),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)));
|
||||
SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,10 @@ extension ClientStoriesExtension on Client {
|
||||
|
||||
List<User> get contacts => rooms
|
||||
.where((room) => room.isDirectChat)
|
||||
.map((room) =>
|
||||
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!))
|
||||
.map(
|
||||
(room) =>
|
||||
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!),
|
||||
)
|
||||
.toList();
|
||||
|
||||
List<Room> get storiesRooms =>
|
||||
@ -78,23 +80,30 @@ extension ClientStoriesExtension on Client {
|
||||
}
|
||||
|
||||
Future<Room?> getStoriesRoom(BuildContext context) async {
|
||||
final candidates = rooms.where((room) =>
|
||||
room.getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') ==
|
||||
storiesRoomType &&
|
||||
room.ownPowerLevel >= 100);
|
||||
final candidates = rooms.where(
|
||||
(room) =>
|
||||
room
|
||||
.getState(EventTypes.RoomCreate)
|
||||
?.content
|
||||
.tryGet<String>('type') ==
|
||||
storiesRoomType &&
|
||||
room.ownPowerLevel >= 100,
|
||||
);
|
||||
if (candidates.isEmpty) return null;
|
||||
if (candidates.length == 1) return candidates.single;
|
||||
return await showModalActionSheet<Room>(
|
||||
context: context,
|
||||
actions: candidates
|
||||
.map(
|
||||
(room) => SheetAction(
|
||||
label: room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
key: room),
|
||||
)
|
||||
.toList());
|
||||
context: context,
|
||||
actions: candidates
|
||||
.map(
|
||||
(room) => SheetAction(
|
||||
label: room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
key: room,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,8 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
|
||||
static const String _cipherStorageKey = 'hive_encryption_key';
|
||||
|
||||
static Future<FlutterHiveCollectionsDatabase> databaseBuilder(
|
||||
Client client) async {
|
||||
Client client,
|
||||
) async {
|
||||
Logs().d('Open Hive...');
|
||||
HiveAesCipher? hiverCipher;
|
||||
try {
|
||||
@ -96,9 +97,9 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
|
||||
}
|
||||
}
|
||||
// do not destroy your stable FluffyChat in debug mode
|
||||
directory = Directory(directory.uri
|
||||
.resolve(kDebugMode ? 'hive_debug' : 'hive')
|
||||
.toFilePath());
|
||||
directory = Directory(
|
||||
directory.uri.resolve(kDebugMode ? 'hive_debug' : 'hive').toFilePath(),
|
||||
);
|
||||
directory.create(recursive: true);
|
||||
path = directory.path;
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ extension IosBadgeClientExtension on Client {
|
||||
void updateIosBadge() {
|
||||
if (PlatformInfos.isIOS) {
|
||||
// Workaround for iOS not clearing notifications with fcm_shared_isolate
|
||||
if (!rooms.any((r) =>
|
||||
r.membership == Membership.invite || (r.notificationCount > 0))) {
|
||||
if (!rooms.any(
|
||||
(r) => r.membership == Membership.invite || (r.notificationCount > 0),
|
||||
)) {
|
||||
// ignore: unawaited_futures
|
||||
FlutterLocalNotificationsPlugin().cancelAll();
|
||||
FlutterAppBadger.removeBadge();
|
||||
|
@ -62,7 +62,9 @@ class MatrixLocals extends MatrixLocalizations {
|
||||
|
||||
@override
|
||||
String changedTheGuestAccessRulesTo(
|
||||
String senderName, String localizedString) {
|
||||
String senderName,
|
||||
String localizedString,
|
||||
) {
|
||||
return l10n.changedTheGuestAccessRulesTo(senderName, localizedString);
|
||||
}
|
||||
|
||||
@ -73,7 +75,9 @@ class MatrixLocals extends MatrixLocalizations {
|
||||
|
||||
@override
|
||||
String changedTheHistoryVisibilityTo(
|
||||
String senderName, String localizedString) {
|
||||
String senderName,
|
||||
String localizedString,
|
||||
) {
|
||||
return l10n.changedTheHistoryVisibilityTo(senderName, localizedString);
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,9 @@ Future<void> _tryPushHelper(
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
final store = await SharedPreferences.getInstance();
|
||||
await store.setString(
|
||||
SettingKeys.notificationCurrentIds, json.encode({}));
|
||||
SettingKeys.notificationCurrentIds,
|
||||
json.encode({}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@ -237,7 +239,8 @@ Future<void> _tryPushHelper(
|
||||
Future<int> mapRoomIdToInt(String roomId) async {
|
||||
final store = await SharedPreferences.getInstance();
|
||||
final idMap = Map<String, int>.from(
|
||||
jsonDecode(store.getString(SettingKeys.notificationCurrentIds) ?? '{}'));
|
||||
jsonDecode(store.getString(SettingKeys.notificationCurrentIds) ?? '{}'),
|
||||
);
|
||||
int? currentInt;
|
||||
try {
|
||||
currentInt = idMap[roomId];
|
||||
|
@ -54,12 +54,14 @@ extension RoomStatusExtension on Room {
|
||||
}
|
||||
} else if (typingUsers.length == 2) {
|
||||
typingText = L10n.of(context)!.userAndUserAreTyping(
|
||||
typingUsers.first.calcDisplayname(),
|
||||
typingUsers[1].calcDisplayname());
|
||||
typingUsers.first.calcDisplayname(),
|
||||
typingUsers[1].calcDisplayname(),
|
||||
);
|
||||
} else if (typingUsers.length > 2) {
|
||||
typingText = L10n.of(context)!.userAndOthersAreTyping(
|
||||
typingUsers.first.calcDisplayname(),
|
||||
(typingUsers.length - 1).toString());
|
||||
typingUsers.first.calcDisplayname(),
|
||||
(typingUsers.length - 1).toString(),
|
||||
);
|
||||
}
|
||||
return typingText;
|
||||
}
|
||||
@ -76,8 +78,10 @@ extension RoomStatusExtension on Room {
|
||||
break;
|
||||
}
|
||||
}
|
||||
lastReceipts.removeWhere((user) =>
|
||||
user.id == client.userID || user.id == timeline.events.first.senderId);
|
||||
lastReceipts.removeWhere(
|
||||
(user) =>
|
||||
user.id == client.userID || user.id == timeline.events.first.senderId,
|
||||
);
|
||||
return lastReceipts.toList();
|
||||
}
|
||||
}
|
||||
|
@ -33,9 +33,11 @@ extension StreamExtension on Stream {
|
||||
gotMessage = true;
|
||||
}
|
||||
};
|
||||
final subscription = listen((_) => onMessage?.call(),
|
||||
onDone: () => controller.close(),
|
||||
onError: (e, s) => controller.addError(e, s));
|
||||
final subscription = listen(
|
||||
(_) => onMessage?.call(),
|
||||
onDone: () => controller.close(),
|
||||
onError: (e, s) => controller.addError(e, s),
|
||||
);
|
||||
// add proper cleanup to the subscription and the controller, to not memory leak
|
||||
controller.onCancel = () {
|
||||
subscription.cancel();
|
||||
|
@ -82,7 +82,8 @@ extension UiaRequestManager on MatrixState {
|
||||
);
|
||||
default:
|
||||
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());
|
||||
if (OkCancelResult.ok ==
|
||||
await showOkCancelAlertDialog(
|
||||
|
@ -20,7 +20,8 @@ class UpdateCheckerNoStore {
|
||||
static const gitLabHost = 'gitlab.com';
|
||||
|
||||
static Uri get tagsUri => Uri.parse(
|
||||
'https://$gitLabHost/projects/$gitLabProjectId/repository/tags');
|
||||
'https://$gitLabHost/projects/$gitLabProjectId/repository/tags',
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
|
@ -33,7 +33,8 @@ class UrlLauncher {
|
||||
if (uri == null) {
|
||||
// we can't open this thing
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))));
|
||||
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
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
|
||||
// geo URIs
|
||||
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;
|
||||
}
|
||||
@ -71,7 +73,8 @@ class UrlLauncher {
|
||||
}
|
||||
if (uri.host.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))));
|
||||
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// okay, we have either an http or an https URI.
|
||||
@ -86,8 +89,10 @@ class UrlLauncher {
|
||||
}).join('.');
|
||||
// Force LaunchMode.externalApplication, otherwise url_launcher will default
|
||||
// to opening links in a webview on mobile platforms.
|
||||
launchUrlString(uri.replace(host: newHost).toString(),
|
||||
mode: LaunchMode.externalApplication);
|
||||
launchUrlString(
|
||||
uri.replace(host: newHost).toString(),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
void openMatrixToUrl() async {
|
||||
@ -142,8 +147,10 @@ class UrlLauncher {
|
||||
}
|
||||
// we have the room, so....just open it
|
||||
if (event != null) {
|
||||
VRouter.of(context).toSegments(['rooms', room.id],
|
||||
queryParameters: {'event': event});
|
||||
VRouter.of(context).toSegments(
|
||||
['rooms', room.id],
|
||||
queryParameters: {'event': event},
|
||||
);
|
||||
} else {
|
||||
VRouter.of(context).toSegments(['rooms', room.id]);
|
||||
}
|
||||
@ -175,11 +182,14 @@ class UrlLauncher {
|
||||
if (response.error != null) return;
|
||||
// wait for two seconds so that it probably came down /sync
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Future.delayed(const Duration(seconds: 2)));
|
||||
context: context,
|
||||
future: () => Future.delayed(const Duration(seconds: 2)),
|
||||
);
|
||||
if (event != null) {
|
||||
VRouter.of(context).toSegments(['rooms', response.result!],
|
||||
queryParameters: {'event': event});
|
||||
VRouter.of(context).toSegments(
|
||||
['rooms', response.result!],
|
||||
queryParameters: {'event': event},
|
||||
);
|
||||
} else {
|
||||
VRouter.of(context).toSegments(['rooms', response.result!]);
|
||||
}
|
||||
|
@ -111,14 +111,15 @@ class CallKeepManager {
|
||||
Future<void> showCallkitIncoming(CallSession call) async {
|
||||
if (!setupDone) {
|
||||
await _callKeep.setup(
|
||||
null,
|
||||
<String, dynamic>{
|
||||
'ios': <String, dynamic>{
|
||||
'appName': appName,
|
||||
},
|
||||
'android': alertOptions,
|
||||
null,
|
||||
<String, dynamic>{
|
||||
'ios': <String, dynamic>{
|
||||
'appName': appName,
|
||||
},
|
||||
backgroundMode: true);
|
||||
'android': alertOptions,
|
||||
},
|
||||
backgroundMode: true,
|
||||
);
|
||||
}
|
||||
setupDone = true;
|
||||
await displayIncomingCall(call);
|
||||
@ -131,7 +132,8 @@ class CallKeepManager {
|
||||
(event) {
|
||||
if (event == CallEvent.kLocalHoldUnhold) {
|
||||
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(CallKeepDidPerformDTMFAction(), didPerformDTMFAction);
|
||||
_callKeep.on(
|
||||
CallKeepDidReceiveStartCallAction(), didReceiveStartCallAction);
|
||||
CallKeepDidReceiveStartCallAction(),
|
||||
didReceiveStartCallAction,
|
||||
);
|
||||
_callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction);
|
||||
_callKeep.on(
|
||||
CallKeepDidPerformSetMutedCallAction(), didPerformSetMutedCallAction);
|
||||
CallKeepDidPerformSetMutedCallAction(),
|
||||
didPerformSetMutedCallAction,
|
||||
);
|
||||
_callKeep.on(CallKeepPerformEndCallAction(), endCall);
|
||||
_callKeep.on(CallKeepPushKitToken(), onPushKitToken);
|
||||
_callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall);
|
||||
@ -209,11 +215,17 @@ class CallKeepManager {
|
||||
Future<void> updateDisplay(String callUUID) async {
|
||||
// Workaround because Android doesn't display well displayName, se we have to switch ...
|
||||
if (isIOS) {
|
||||
await _callKeep.updateDisplay(callUUID,
|
||||
displayName: 'New Name', handle: callUUID);
|
||||
await _callKeep.updateDisplay(
|
||||
callUUID,
|
||||
displayName: 'New Name',
|
||||
handle: callUUID,
|
||||
);
|
||||
} else {
|
||||
await _callKeep.updateDisplay(callUUID,
|
||||
displayName: callUUID, handle: 'New Name');
|
||||
await _callKeep.updateDisplay(
|
||||
callUUID,
|
||||
displayName: callUUID,
|
||||
handle: 'New Name',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,7 +262,8 @@ class CallKeepManager {
|
||||
const Divider(),
|
||||
ListTile(
|
||||
onTap: () => FlutterForegroundTask.openSystemAlertWindowSettings(
|
||||
forceOpen: true),
|
||||
forceOpen: true,
|
||||
),
|
||||
title: Text(L10n.of(context)!.appearOnTop),
|
||||
subtitle: Text(L10n.of(context)!.appearOnTopDetails),
|
||||
trailing: const Icon(Icons.file_upload_rounded),
|
||||
@ -310,7 +323,8 @@ class CallKeepManager {
|
||||
}
|
||||
|
||||
Future<void> didReceiveStartCallAction(
|
||||
CallKeepDidReceiveStartCallAction event) async {
|
||||
CallKeepDidReceiveStartCallAction event,
|
||||
) async {
|
||||
if (event.handle == null) {
|
||||
// @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined`
|
||||
return;
|
||||
@ -328,7 +342,8 @@ class CallKeepManager {
|
||||
}
|
||||
|
||||
Future<void> didPerformSetMutedCallAction(
|
||||
CallKeepDidPerformSetMutedCallAction event) async {
|
||||
CallKeepDidPerformSetMutedCallAction event,
|
||||
) async {
|
||||
final keeper = calls[event.callUUID];
|
||||
if (event.muted!) {
|
||||
keeper!.call.setMicrophoneMuted(true);
|
||||
@ -339,7 +354,8 @@ class CallKeepManager {
|
||||
}
|
||||
|
||||
Future<void> didToggleHoldCallAction(
|
||||
CallKeepDidToggleHoldAction event) async {
|
||||
CallKeepDidToggleHoldAction event,
|
||||
) async {
|
||||
final keeper = calls[event.callUUID];
|
||||
if (event.hold!) {
|
||||
keeper!.call.setRemoteOnHold(true);
|
||||
|
@ -82,14 +82,15 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
||||
} else {
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (_) => Calling(
|
||||
context: context,
|
||||
client: client,
|
||||
callId: callId,
|
||||
call: call,
|
||||
onClear: () {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
}),
|
||||
context: context,
|
||||
client: client,
|
||||
callId: callId,
|
||||
call: call,
|
||||
onClear: () {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
},
|
||||
),
|
||||
);
|
||||
Overlay.of(context).insert(overlayEntry!);
|
||||
}
|
||||
@ -103,8 +104,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
||||
|
||||
@override
|
||||
Future<RTCPeerConnection> createPeerConnection(
|
||||
Map<String, dynamic> configuration,
|
||||
[Map<String, dynamic> constraints = const {}]) =>
|
||||
Map<String, dynamic> configuration, [
|
||||
Map<String, dynamic> constraints = const {},
|
||||
]) =>
|
||||
webrtc_impl.createPeerConnection(configuration, constraints);
|
||||
|
||||
@override
|
||||
@ -150,7 +152,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
||||
try {
|
||||
final wasForeground = await FlutterForegroundTask.isAppOnForeground;
|
||||
await Store().setItem(
|
||||
'wasForeground', wasForeground == true ? 'true' : 'false');
|
||||
'wasForeground',
|
||||
wasForeground == true ? 'true' : 'false',
|
||||
);
|
||||
FlutterForegroundTask.setOnLockScreenVisibility(true);
|
||||
FlutterForegroundTask.wakeUpScreen();
|
||||
FlutterForegroundTask.launchApp();
|
||||
@ -162,10 +166,13 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
|
||||
try {
|
||||
if (!hasCallingAccount) {
|
||||
ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!)
|
||||
.showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'No calling accounts found (used for native calls UI)',
|
||||
)));
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'No calling accounts found (used for native calls UI)',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
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