Watch
1
0
Fork
You've already forked nexus
0
forked from Nexus/nexus

Compare commits

...
Author SHA1 Message Date
716541b457
fixup oauth animation 2026-07-24 10:13:20 -04:00
965c7ea2dc
add media for twim 2026-07-24 09:59:50 -04:00
85d02f2ac9
bump gomuks 2026-07-23 15:29:23 -04:00
1c446c4cdf
bump gomuks
fixes no loading issue when restarting verification after closing app
2026-07-23 13:58:33 -04:00
51430d2830
Verify state on login
This fixes "already logged in errors" if you started logging in before cancelling, and then trying again.
2026-07-22 21:53:42 -04:00
4e208764f5
remove outdated comment 2026-07-22 16:45:47 -04:00
eecc307f04
disable provider auto retry 2026-07-22 16:35:18 -04:00
6221eb4a7a
bump gomuks
Fixes overly aggressive caching on login, causing issues when changing login server
2026-07-22 16:19:06 -04:00
370549072e
make logout properly pop all settings on mobile 2026-07-22 15:57:21 -04:00
80b938c0be
fix wrong settings categories opening when multiple sections exist 2026-07-22 15:37:50 -04:00
87e1ef0d9b
clean up if statements into one 2026-07-22 12:00:35 -04:00
f8c705531c
ignore setState during build
It seems to happen randomly right now.
2026-07-22 11:59:10 -04:00
2a91959333
fix encrypted uploads 2026-07-21 21:34:49 -04:00
34ddd6d32b
change font weight for tooltips 2026-07-21 20:57:58 -04:00
a8726feef6
allow sending messages without a body if they have an attachment 2026-07-21 20:27:21 -04:00
bd1bad76d7
add camera picking support
Not tested.
2026-07-21 20:21:16 -04:00
c4f63d18b5
add support for sending media
Still needs camera support.
2026-07-21 20:19:01 -04:00
a8285548e8
custom style for tooltips 2026-07-21 18:25:38 -04:00
cdad9b760e
add settings to login page, but gate login settings 2026-07-21 18:00:52 -04:00
06c5411dfc
fix bug flutter bump introduced in mention overlay 2026-07-21 16:28:05 -04:00
f6b5bd4057
bump gomuks 2026-07-21 16:27:53 -04:00
0da3b4da22
don't require non-null metadata 2026-07-21 16:25:15 -04:00
25 changed files with 493 additions and 239 deletions

View file

@ -48,7 +48,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
- [x] Replies - [x] Replies
- [x] Choose ping on/off - [x] Choose ping on/off
- [x] Per message profiles - [x] Per message profiles
- [ ] Attachments - [x] Attachments
- [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391) - [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391)
- [x] Mentions - [x] Mentions
- [x] Users - [x] Users

BIN
assets/twim/oauth.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/twim/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

2
gomuks

@ -1 +1 @@
Subproject commit 3c759ba03a5235deb82cb1559ebc42c36ee0e874 Subproject commit c06eb1cbda2d49ff199a351e2d55df6ff7ab602b

View file

@ -0,0 +1,40 @@
import "package:file_selector/file_selector.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/message.dart";
import "package:path/path.dart";
class AttachmentController extends Notifier<(String, MessageContent?)?> {
final String roomId;
AttachmentController(this.roomId);
@override
Null build() => null;
Future<void> add(XFile file) async {
final filename = basename(file.path);
state = (filename, null);
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
final content = await ref
.watch(ClientController.provider.notifier)
.uploadMedia(
.new(
path: file.path,
encrypt: room?.state[EventType.encryption.type]?.isNotEmpty == true,
),
);
state = (filename, content);
}
static final provider = NotifierProvider.family
.autoDispose<AttachmentController, (String, MessageContent?)?, String>(
AttachmentController.new,
);
}

View file

@ -23,10 +23,8 @@ class AuthUrlController extends AsyncNotifier<OAuthAuthCodeResponse> {
), ),
); );
static final provider = static final provider = AsyncNotifierProvider.family
AsyncNotifierProvider.family< .autoDispose<AuthUrlController, OAuthAuthCodeResponse, Uri>(
AuthUrlController, AuthUrlController.new,
OAuthAuthCodeResponse, );
Uri
>(AuthUrlController.new);
} }

View file

@ -14,6 +14,7 @@ import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart"; import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/oauth_auth_code_response.dart"; import "package:nexus/models/oauth_auth_code_response.dart";
import "package:nexus/models/paginate.dart"; import "package:nexus/models/paginate.dart";
@ -32,6 +33,7 @@ import "package:nexus/models/requests/send_event.dart";
import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/requests/send_message.dart";
import "package:nexus/models/requests/set_membership.dart"; import "package:nexus/models/requests/set_membership.dart";
import "package:nexus/models/requests/set_state.dart"; import "package:nexus/models/requests/set_state.dart";
import "package:nexus/models/requests/upload_media.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/models/sync_data.dart"; import "package:nexus/models/sync_data.dart";
import "package:nexus/src/third_party/gomuks.g.dart"; import "package:nexus/src/third_party/gomuks.g.dart";
@ -266,6 +268,9 @@ class ClientController extends AsyncNotifier<int> {
Future<void> setMembership(SetMembershipRequest request) => Future<void> setMembership(SetMembershipRequest request) =>
_sendCommand("set_membership", request.toJson()); _sendCommand("set_membership", request.toJson());
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
.fromJson(await _sendCommand("upload_media", request.toJson()));
Future<void> logout() => _sendCommand("logout"); Future<void> logout() => _sendCommand("logout");
Future<void> markRead(Room room) async { Future<void> markRead(Room room) async {

View file

@ -0,0 +1,11 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:image_picker/image_picker.dart";
class ImagePickerController extends Notifier<ImagePicker> {
@override
ImagePicker build() => .new();
static final provider = NotifierProvider<ImagePickerController, ImagePicker>(
ImagePickerController.new,
);
}

View file

@ -4,6 +4,7 @@ import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:fluttertagger/fluttertagger.dart"; import "package:fluttertagger/fluttertagger.dart";
import "package:nexus/controllers/attachment.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/content.dart";
@ -120,6 +121,10 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
required RelationType relationType, required RelationType relationType,
Event? relation, Event? relation,
}) async { }) async {
final attachment = ref.watch(AttachmentController.provider(roomId));
if (attachment != null && attachment.$2 == null) return;
var taggedMessage = text; var taggedMessage = text;
for (final tag in tags) { for (final tag in tags) {
@ -136,6 +141,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
final event = await client.sendMessage( final event = await client.sendMessage(
SendMessageRequest( SendMessageRequest(
roomId: roomId, roomId: roomId,
baseContent: attachment?.$2,
mentions: Mentions( mentions: Mentions(
userIds: [ userIds: [
if (shouldMention == true && if (shouldMention == true &&
@ -152,6 +158,8 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
), ),
); );
ref.invalidate(AttachmentController.provider(roomId));
ref ref
.watch(RoomsController.provider.notifier) .watch(RoomsController.provider.notifier)
.update( .update(

View file

@ -5,6 +5,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:m3e_buttons/m3e_buttons.dart"; import "package:m3e_buttons/m3e_buttons.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/settings.dart";
import "package:nexus/models/settings_category.dart"; import "package:nexus/models/settings_category.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
@ -89,6 +90,7 @@ class SettingsSectionsController
]), ]),
), ),
]), ]),
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
"Account": .new([ "Account": .new([
.new(title: "Profile", icon: Icons.person, settings: .new([])), .new(title: "Profile", icon: Icons.person, settings: .new([])),
.new( .new(
@ -104,10 +106,15 @@ class SettingsSectionsController
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return M3EButton.icon( return M3EButton.icon(
onPressed: () async { onPressed: () async {
Navigator.of(
context,
).popUntil((route) => route.isFirst);
await WidgetsBinding.instance.endOfFrame;
await ref await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.logout(); .logout();
if (context.mounted) Navigator.of(context).pop();
}, },
label: Text(title), label: Text(title),
icon: Icon(icon), icon: Icon(icon),

View file

@ -1,7 +1,12 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
extension SchemeToTheme on ColorScheme { extension SchemeToTheme on ColorScheme {
ThemeData get theme => .from(colorScheme: this).copyWith( ThemeData get theme {
final textTheme = ThemeData(
fontFamilyFallback: ["sans", "emoji", "fallback-sans", "fallback-emoji"],
brightness: brightness,
).textTheme;
return .from(colorScheme: this).copyWith(
cardTheme: .new(color: primaryContainer), cardTheme: .new(color: primaryContainer),
popupMenuTheme: .new( popupMenuTheme: .new(
shape: RoundedRectangleBorder(borderRadius: .circular(16)), shape: RoundedRectangleBorder(borderRadius: .circular(16)),
@ -11,12 +16,21 @@ extension SchemeToTheme on ColorScheme {
titleSpacing: 0, titleSpacing: 0,
backgroundColor: surfaceContainerLow, backgroundColor: surfaceContainerLow,
), ),
textTheme: ThemeData( tooltipTheme: .new(
fontFamilyFallback: ["sans", "emoji", "fallback-sans", "fallback-emoji"], textStyle: textTheme.labelLarge?.copyWith(
brightness: brightness, fontSize: 16,
).textTheme, fontWeight: .w600,
),
padding: .all(8),
decoration: BoxDecoration(
color: surfaceContainerHighest,
borderRadius: .circular(8),
),
),
textTheme: textTheme,
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
); );
}
} }

View file

@ -38,11 +38,16 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue}
} }
void showError(Object error, [StackTrace? stackTrace]) { void showError(Object error, [StackTrace? stackTrace]) {
if (error.toString().contains("DioException")) return; if (error.toString().contains("DioException") ||
if (error.toString().contains("Invalid source")) return; error.toString().contains(
if (error.toString().contains("UTF-16")) return; "setState() or markNeedsBuild() called during build.",
if (error.toString().contains("HTTP request failed")) return; ) ||
if (error.toString().contains("Invalid image data")) return; error.toString().contains("Invalid source") ||
error.toString().contains("UTF-16") ||
error.toString().contains("HTTP request failed") ||
error.toString().contains("Invalid image data")) {
return;
}
debugPrintStack(stackTrace: stackTrace, label: error.toString()); debugPrintStack(stackTrace: stackTrace, label: error.toString());
if (navigatorKey.currentContext != null) { if (navigatorKey.currentContext != null) {
@ -79,7 +84,7 @@ void main() async {
runApp( runApp(
ProviderScope( ProviderScope(
retry: null, retry: (_, _) => null,
observers: [ observers: [
// Change false to true if you want debug information on provider reloads // Change false to true if you want debug information on provider reloads
// ignore: dead_code // ignore: dead_code

View file

@ -7,6 +7,8 @@ import "package:nexus/models/info/video.dart";
part "message.freezed.dart"; part "message.freezed.dart";
part "message.g.dart"; part "message.g.dart";
typedef EncryptedFile = Map<String, dynamic>;
@Freezed(unionKey: "msgtype", fallbackUnion: "default") @Freezed(unionKey: "msgtype", fallbackUnion: "default")
abstract class MessageContent extends Content with _$MessageContent { abstract class MessageContent extends Content with _$MessageContent {
MessageContent._(); MessageContent._();
@ -38,7 +40,7 @@ abstract class MessageContent extends Content with _$MessageContent {
required String body, required String body,
MessageFormat? format, MessageFormat? format,
String? formattedBody, String? formattedBody,
// EncryptedFile? file EncryptedFile? file,
String? filename, String? filename,
ImageInfo? info, ImageInfo? info,
Uri? url, Uri? url,
@ -49,7 +51,7 @@ abstract class MessageContent extends Content with _$MessageContent {
required String body, required String body,
MessageFormat? format, MessageFormat? format,
String? formattedBody, String? formattedBody,
// EncryptedFile? file EncryptedFile? file,
String? filename, String? filename,
FileInfo? info, FileInfo? info,
Uri? url, Uri? url,
@ -60,7 +62,7 @@ abstract class MessageContent extends Content with _$MessageContent {
required String body, required String body,
MessageFormat? format, MessageFormat? format,
String? formattedBody, String? formattedBody,
// EncryptedFile? file EncryptedFile? file,
String? filename, String? filename,
AudioInfo? info, AudioInfo? info,
Uri? url, Uri? url,
@ -71,7 +73,7 @@ abstract class MessageContent extends Content with _$MessageContent {
required String body, required String body,
MessageFormat? format, MessageFormat? format,
String? formattedBody, String? formattedBody,
// EncryptedFile? file EncryptedFile? file,
String? filename, String? filename,
VideoInfo? info, VideoInfo? info,
Uri? url, Uri? url,

View file

@ -1,5 +1,6 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
part "send_message.freezed.dart"; part "send_message.freezed.dart";
part "send_message.g.dart"; part "send_message.g.dart";
@ -9,6 +10,7 @@ abstract class SendMessageRequest with _$SendMessageRequest {
const factory SendMessageRequest({ const factory SendMessageRequest({
required String roomId, required String roomId,
required String text, required String text,
Content? baseContent,
@Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions, @Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions,
@JsonKey(name: "relates_to") Relation? relation, @JsonKey(name: "relates_to") Relation? relation,
}) = _SendMessageRequest; }) = _SendMessageRequest;

View file

@ -0,0 +1,24 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "upload_media.freezed.dart";
part "upload_media.g.dart";
@freezed
abstract class UploadMediaRequest with _$UploadMediaRequest {
const factory UploadMediaRequest({
required String path,
required bool encrypt,
String? filename,
@Default(false) @JsonKey(name: "voice_message") bool isVoiceMessage,
@Default(false) bool forceFile,
// Below params only work if encodeTo is set
String? encodeTo,
int? resizeWidth,
int? resizeHeight,
int? resizePercent,
@Default(80) int quality,
}) = _UploadMediaRequest;
factory UploadMediaRequest.fromJson(Map<String, Object?> json) =>
_$UploadMediaRequestFromJson(json);
}

View file

@ -9,6 +9,7 @@ import "package:nexus/controllers/client_id.dart";
import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:nexus/models/homeserver.dart"; import "package:nexus/models/homeserver.dart";
import "package:nexus/pages/settings.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/divider_text.dart";
@ -57,6 +58,9 @@ class SelectServerPage extends HookConsumerWidget {
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url);
AppLinks().uriLinkStream.listen((encodedUri) async { AppLinks().uriLinkStream.listen((encodedUri) async {
final state = encodedUri.queryParameters["state"];
if (state != codeResponse.state) return;
final code = encodedUri.queryParameters["code"]; final code = encodedUri.queryParameters["code"];
if (code != null) { if (code != null) {
@ -79,9 +83,6 @@ class SelectServerPage extends HookConsumerWidget {
.onError(showError); .onError(showError);
} }
}); });
// await Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)),
// );
} }
} }
} catch (error, stackTrace) { } catch (error, stackTrace) {
@ -92,7 +93,15 @@ class SelectServerPage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
appBar: Appbar(), appBar: Appbar(
actions: .new([
IconButton(
onPressed: () =>
showDialog(context: context, builder: (_) => SettingsPage()),
icon: Icon(Icons.settings),
),
]),
),
body: SafeArea( body: SafeArea(
child: Center( child: Center(
child: ConstrainedBox( child: ConstrainedBox(

View file

@ -127,13 +127,13 @@ class SettingsPage extends ConsumerWidget {
), ),
...sections ...sections
.mapTo( .mapTo(
(categoryGroup, categories) => [ (section, categories) => [
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
).copyWith(bottom: 4), ).copyWith(bottom: 4),
child: DividerText(categoryGroup), child: DividerText(section),
), ),
), ),
SliverM3ECardList( SliverM3ECardList(
@ -150,7 +150,14 @@ class SettingsPage extends ConsumerWidget {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
SettingsCategoryPage(index), SettingsCategoryPage(
sections
.values
.flattenedToList
.indexOf(
categories[index],
),
),
), ),
), ),
itemBuilder: (context, index) => ListTile( itemBuilder: (context, index) => ListTile(

View file

@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/pages/settings.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/helpers/required_validator_helper.dart"; import "package:nexus/helpers/required_validator_helper.dart";
@ -32,7 +33,15 @@ class VerifyPage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
appBar: Appbar(), appBar: Appbar(
actions: .new([
IconButton(
onPressed: () =>
showDialog(context: context, builder: (_) => SettingsPage()),
icon: Icon(Icons.settings),
),
]),
),
body: AlertDialog( body: AlertDialog(
title: Text("Verify"), title: Text("Verify"),
content: Form( content: Form(

View file

@ -1,9 +1,13 @@
import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:file_selector/file_selector.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter/services.dart"; import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:fluttertagger/fluttertagger.dart"; import "package:fluttertagger/fluttertagger.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/attachment.dart";
import "package:nexus/controllers/image_picker.dart";
import "package:nexus/controllers/power_level.dart"; import "package:nexus/controllers/power_level.dart";
import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
@ -11,6 +15,7 @@ import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/composer/mention_overlay.dart"; import "package:nexus/widgets/composer/mention_overlay.dart";
import "package:nexus/widgets/composer/relation_preview.dart"; import "package:nexus/widgets/composer/relation_preview.dart";
import "package:nexus/widgets/emoji_picker_button.dart"; import "package:nexus/widgets/emoji_picker_button.dart";
import "package:nexus/main.dart";
class Composer extends HookConsumerWidget { class Composer extends HookConsumerWidget {
final String roomId; final String roomId;
@ -51,8 +56,10 @@ class Composer extends HookConsumerWidget {
}; };
} }
final attachment = ref.watch(AttachmentController.provider(roomId));
void send() { void send() {
if (controller.value.text.isEmpty) return; if (controller.value.text.isEmpty && attachment == null) return;
onSend( onSend(
controller.value.formattedText, controller.value.formattedText,
shouldMention: shouldMention.value, shouldMention: shouldMention.value,
@ -70,7 +77,28 @@ class Composer extends HookConsumerWidget {
return Padding( return Padding(
padding: .all(12), padding: .all(12),
child: ClipRRect( child: Column(
children: [
if (attachment != null)
Card(
margin: .only(bottom: 8),
child: ListTile(
leading: attachment.$2 == null
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
)
: Icon(Icons.file_copy),
title: Text(attachment.$1),
trailing: IconButton(
onPressed: () =>
ref.invalidate(AttachmentController.provider(roomId)),
icon: Icon(Icons.close),
),
),
),
ClipRRect(
borderRadius: .all(.circular(12)), borderRadius: .all(.circular(12)),
child: Column( child: Column(
children: [ children: [
@ -102,20 +130,58 @@ class Composer extends HookConsumerWidget {
), ),
PopupMenuButton( PopupMenuButton(
tooltip: "Add media", tooltip: "Add media",
enabled: attachment == null,
itemBuilder: (context) => [ itemBuilder: (context) => [
if (Platform.isAndroid || Platform.isIOS)
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
title: Text("Camera"), title: Text("Camera"),
leading: Icon(Icons.add_a_photo), leading: Icon(Icons.add_a_photo),
), ),
onTap: () async => ref
.watch(
AttachmentController.provider(
roomId,
).notifier,
)
.add(
(await ref
.watch(
ImagePickerController.provider,
)
.pickImage(source: .camera))!,
)
.onError(showError),
), ),
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
title: Text("Gallery"), title: Text("Gallery"),
leading: Icon(Icons.add_photo_alternate), leading: Icon(Icons.add_photo_alternate),
), ),
onTap: () async => ref
.watch(
AttachmentController.provider(
roomId,
).notifier,
)
.add(
(await ref
.watch(
ImagePickerController.provider,
)
.pickImage(source: .gallery))!,
)
.onError(showError),
), ),
PopupMenuItem( PopupMenuItem(
onTap: () async => ref
.watch(
AttachmentController.provider(
roomId,
).notifier,
)
.add((await openFile())!)
.onError(showError),
child: ListTile( child: ListTile(
title: Text("Files"), title: Text("Files"),
leading: Icon(Icons.attachment), leading: Icon(Icons.attachment),
@ -141,14 +207,18 @@ class Composer extends HookConsumerWidget {
triggerCharacter.value = newTriggerCharacter; triggerCharacter.value = newTriggerCharacter;
query.value = newQuery; query.value = newQuery;
}, },
triggerCharacterAndStyles: {"@": style, "#": style}, triggerCharacterAndStyles: {
"@": style,
"#": style,
},
builder: (context, key) => Focus( builder: (context, key) => Focus(
onKeyEvent: (_, event) { onKeyEvent: (_, event) {
if (event is KeyDownEvent && if (event is KeyDownEvent &&
event.logicalKey == event.logicalKey ==
LogicalKeyboardKey.enter) { LogicalKeyboardKey.enter) {
final shiftPressed = final shiftPressed = HardwareKeyboard
HardwareKeyboard.instance.isShiftPressed; .instance
.isShiftPressed;
if (!shiftPressed) { if (!shiftPressed) {
send(); send();
@ -174,7 +244,10 @@ class Composer extends HookConsumerWidget {
), ),
), ),
IconButton( IconButton(
onPressed: send, onPressed:
attachment != null && attachment.$2 == null
? null
: send,
icon: Icon(Icons.send), icon: Icon(Icons.send),
tooltip: "Send message", tooltip: "Send message",
), ),
@ -182,7 +255,10 @@ class Composer extends HookConsumerWidget {
: [ : [
Expanded( Expanded(
child: Padding( child: Padding(
padding: .symmetric(horizontal: 8, vertical: 12), padding: .symmetric(
horizontal: 8,
vertical: 12,
),
child: Text( child: Text(
"You don't have permission to send messages in this room...", "You don't have permission to send messages in this room...",
), ),
@ -194,6 +270,8 @@ class Composer extends HookConsumerWidget {
], ],
), ),
), ),
],
),
); );
} }
} }

View file

@ -73,7 +73,9 @@ class MentionOverlay extends ConsumerWidget {
:final displayName, :final displayName,
:final avatarUrl, :final avatarUrl,
) => ) =>
ListTile( Material(
color: Colors.transparent,
child: ListTile(
leading: AvatarOrHash( leading: AvatarOrHash(
avatarUrl, avatarUrl,
displayName ?? displayName ??
@ -89,6 +91,7 @@ class MentionOverlay extends ConsumerWidget {
name: member.stateKey!.localpart, name: member.stateKey!.localpart,
), ),
), ),
),
_ => SizedBox.shrink(), _ => SizedBox.shrink(),
}, },
) )
@ -101,16 +104,21 @@ class MentionOverlay extends ConsumerWidget {
? rooms.values ? rooms.values
: rooms.values.where( : rooms.values.where(
(room) => (room) =>
(room.metadata?.name ?? room.metadata!.id) (room.metadata?.name ??
room.metadata?.id ??
"")
.toLowerCase() .toLowerCase()
.contains(query.toLowerCase()), .contains(query.toLowerCase()),
)) ))
.map((room) { .map((room) {
final name = final name =
room.metadata?.name ?? room.metadata?.name ??
room.metadata!.canonicalAlias ?? room.metadata?.canonicalAlias ??
room.metadata!.id; room.metadata?.id ??
return ListTile( "Unknown Room";
return Material(
color: Colors.transparent,
child: ListTile(
leading: AvatarOrHash( leading: AvatarOrHash(
room.metadata?.avatar, room.metadata?.avatar,
name, name,
@ -135,6 +143,7 @@ class MentionOverlay extends ConsumerWidget {
"", "",
); );
}, },
),
); );
}) })
.toList(), .toList(),

View file

@ -7,7 +7,6 @@ import Foundation
import app_links import app_links
import dynamic_color import dynamic_color
import file_picker
import file_selector_macos import file_selector_macos
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
@ -21,7 +20,6 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))

View file

@ -10,5 +10,7 @@
<true/> <true/>
<key>com.apple.security.network.server</key> <key>com.apple.security.network.server</key>
<true/> <true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict> </dict>
</plist> </plist>

View file

@ -6,5 +6,7 @@
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict> </dict>
</plist> </plist>

View file

@ -402,14 +402,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker: file_selector:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_selector
sha256: fdc6a37f715d19f35b131decf1ce39242eeed5ddae18c0818c3eccb731ab76be sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.0-beta.7" version: "1.1.0"
file_selector_android:
dependency: transitive
description:
name: file_selector_android
sha256: "6a26687fa65cbc28a5345c7ae6f227e89f0b47740978a4c475b1a625da7a331b"
url: "https://pub.dev"
source: hosted
version: "0.5.2+8"
file_selector_ios:
dependency: transitive
description:
name: file_selector_ios
sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca
url: "https://pub.dev"
source: hosted
version: "0.5.3+5"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -434,6 +450,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" version: "2.7.0"
file_selector_web:
dependency: transitive
description:
name: file_selector_web
sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_windows: file_selector_windows:
dependency: transitive dependency: transitive
description: description:

View file

@ -37,8 +37,7 @@ dependencies:
path_provider: 2.1.6 path_provider: 2.1.6
url_launcher: 6.3.2 url_launcher: 6.3.2
freezed_annotation: 3.1.0 freezed_annotation: 3.1.0
image_picker: 1.2.3 image_picker: ^1.2.3
file_picker: 12.0.0-beta.7
path: 1.9.1 path: 1.9.1
dynamic_color: 1.8.1 dynamic_color: 1.8.1
collection: 1.19.1 collection: 1.19.1
@ -78,6 +77,7 @@ dependencies:
xdg_directories: 1.1.0 xdg_directories: 1.1.0
package_info_plus: 10.2.1 package_info_plus: 10.2.1
app_links: 7.2.1 app_links: 7.2.1
file_selector: ^1.1.0
dev_dependencies: dev_dependencies:
build_runner: ^2.15.1 build_runner: ^2.15.1