forked from Nexus/nexus
Compare commits
25 changed files with 239 additions and 493 deletions
|
|
@ -48,7 +48,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
|||
- [x] Replies
|
||||
- [x] Choose ping on/off
|
||||
- [x] Per message profiles
|
||||
- [x] Attachments
|
||||
- [ ] Attachments
|
||||
- [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391)
|
||||
- [x] Mentions
|
||||
- [x] Users
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB |
2
gomuks
2
gomuks
|
|
@ -1 +1 @@
|
|||
Subproject commit c06eb1cbda2d49ff199a351e2d55df6ff7ab602b
|
||||
Subproject commit 3c759ba03a5235deb82cb1559ebc42c36ee0e874
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -23,8 +23,10 @@ class AuthUrlController extends AsyncNotifier<OAuthAuthCodeResponse> {
|
|||
),
|
||||
);
|
||||
|
||||
static final provider = AsyncNotifierProvider.family
|
||||
.autoDispose<AuthUrlController, OAuthAuthCodeResponse, Uri>(
|
||||
AuthUrlController.new,
|
||||
);
|
||||
static final provider =
|
||||
AsyncNotifierProvider.family<
|
||||
AuthUrlController,
|
||||
OAuthAuthCodeResponse,
|
||||
Uri
|
||||
>(AuthUrlController.new);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import "package:nexus/controllers/sync_status.dart";
|
|||
import "package:nexus/controllers/top_level_spaces.dart";
|
||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/oauth_auth_code_response.dart";
|
||||
import "package:nexus/models/paginate.dart";
|
||||
|
|
@ -33,7 +32,6 @@ import "package:nexus/models/requests/send_event.dart";
|
|||
import "package:nexus/models/requests/send_message.dart";
|
||||
import "package:nexus/models/requests/set_membership.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/sync_data.dart";
|
||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||
|
|
@ -268,9 +266,6 @@ class ClientController extends AsyncNotifier<int> {
|
|||
Future<void> setMembership(SetMembershipRequest request) =>
|
||||
_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> markRead(Room room) async {
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import "package:collection/collection.dart";
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:fluttertagger/fluttertagger.dart";
|
||||
import "package:nexus/controllers/attachment.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
|
|
@ -121,10 +120,6 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
|||
required RelationType relationType,
|
||||
Event? relation,
|
||||
}) async {
|
||||
final attachment = ref.watch(AttachmentController.provider(roomId));
|
||||
|
||||
if (attachment != null && attachment.$2 == null) return;
|
||||
|
||||
var taggedMessage = text;
|
||||
|
||||
for (final tag in tags) {
|
||||
|
|
@ -141,7 +136,6 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
|||
final event = await client.sendMessage(
|
||||
SendMessageRequest(
|
||||
roomId: roomId,
|
||||
baseContent: attachment?.$2,
|
||||
mentions: Mentions(
|
||||
userIds: [
|
||||
if (shouldMention == true &&
|
||||
|
|
@ -158,8 +152,6 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
|||
),
|
||||
);
|
||||
|
||||
ref.invalidate(AttachmentController.provider(roomId));
|
||||
|
||||
ref
|
||||
.watch(RoomsController.provider.notifier)
|
||||
.update(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
|||
import "package:intl/intl.dart";
|
||||
import "package:m3e_buttons/m3e_buttons.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/controllers/settings.dart";
|
||||
import "package:nexus/models/settings_category.dart";
|
||||
import "package:nexus/main.dart";
|
||||
|
|
@ -90,47 +89,41 @@ class SettingsSectionsController
|
|||
]),
|
||||
),
|
||||
]),
|
||||
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
|
||||
"Account": .new([
|
||||
.new(title: "Profile", icon: Icons.person, settings: .new([])),
|
||||
.new(
|
||||
title: "Other",
|
||||
icon: Icons.key,
|
||||
settings: .new([
|
||||
.new(
|
||||
title: "Log Out",
|
||||
description:
|
||||
"Log out of your account, returning you to the login page.",
|
||||
builder: (title, description, icon) => Builder(
|
||||
builder: (context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return M3EButton.icon(
|
||||
onPressed: () async {
|
||||
Navigator.of(
|
||||
context,
|
||||
).popUntil((route) => route.isFirst);
|
||||
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
|
||||
await ref
|
||||
.watch(ClientController.provider.notifier)
|
||||
.logout();
|
||||
},
|
||||
label: Text(title),
|
||||
icon: Icon(icon),
|
||||
tooltip: description,
|
||||
decoration: .styleFrom(
|
||||
backgroundColor: colorScheme.errorContainer,
|
||||
foregroundColor: colorScheme.onErrorContainer,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
icon: Icons.logout,
|
||||
"Account": .new([
|
||||
.new(title: "Profile", icon: Icons.person, settings: .new([])),
|
||||
.new(
|
||||
title: "Other",
|
||||
icon: Icons.key,
|
||||
settings: .new([
|
||||
.new(
|
||||
title: "Log Out",
|
||||
description:
|
||||
"Log out of your account, returning you to the login page.",
|
||||
builder: (title, description, icon) => Builder(
|
||||
builder: (context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return M3EButton.icon(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.watch(ClientController.provider.notifier)
|
||||
.logout();
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
},
|
||||
label: Text(title),
|
||||
icon: Icon(icon),
|
||||
tooltip: description,
|
||||
decoration: .styleFrom(
|
||||
backgroundColor: colorScheme.errorContainer,
|
||||
foregroundColor: colorScheme.onErrorContainer,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
icon: Icons.logout,
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,22 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
extension SchemeToTheme on ColorScheme {
|
||||
ThemeData get theme {
|
||||
final textTheme = ThemeData(
|
||||
ThemeData get theme => .from(colorScheme: this).copyWith(
|
||||
cardTheme: .new(color: primaryContainer),
|
||||
popupMenuTheme: .new(
|
||||
shape: RoundedRectangleBorder(borderRadius: .circular(16)),
|
||||
color: surfaceContainerHigh,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
titleSpacing: 0,
|
||||
backgroundColor: surfaceContainerLow,
|
||||
),
|
||||
textTheme: ThemeData(
|
||||
fontFamilyFallback: ["sans", "emoji", "fallback-sans", "fallback-emoji"],
|
||||
brightness: brightness,
|
||||
).textTheme;
|
||||
return .from(colorScheme: this).copyWith(
|
||||
cardTheme: .new(color: primaryContainer),
|
||||
popupMenuTheme: .new(
|
||||
shape: RoundedRectangleBorder(borderRadius: .circular(16)),
|
||||
color: surfaceContainerHigh,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
titleSpacing: 0,
|
||||
backgroundColor: surfaceContainerLow,
|
||||
),
|
||||
tooltipTheme: .new(
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: .w600,
|
||||
),
|
||||
padding: .all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: surfaceContainerHighest,
|
||||
borderRadius: .circular(8),
|
||||
),
|
||||
),
|
||||
textTheme: textTheme,
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
).textTheme,
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,16 +38,11 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue}
|
|||
}
|
||||
|
||||
void showError(Object error, [StackTrace? stackTrace]) {
|
||||
if (error.toString().contains("DioException") ||
|
||||
error.toString().contains(
|
||||
"setState() or markNeedsBuild() called during build.",
|
||||
) ||
|
||||
error.toString().contains("Invalid source") ||
|
||||
error.toString().contains("UTF-16") ||
|
||||
error.toString().contains("HTTP request failed") ||
|
||||
error.toString().contains("Invalid image data")) {
|
||||
return;
|
||||
}
|
||||
if (error.toString().contains("DioException")) return;
|
||||
if (error.toString().contains("Invalid source")) return;
|
||||
if (error.toString().contains("UTF-16")) return;
|
||||
if (error.toString().contains("HTTP request failed")) return;
|
||||
if (error.toString().contains("Invalid image data")) return;
|
||||
|
||||
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
||||
if (navigatorKey.currentContext != null) {
|
||||
|
|
@ -84,7 +79,7 @@ void main() async {
|
|||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
retry: (_, _) => null,
|
||||
retry: null,
|
||||
observers: [
|
||||
// Change false to true if you want debug information on provider reloads
|
||||
// ignore: dead_code
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import "package:nexus/models/info/video.dart";
|
|||
part "message.freezed.dart";
|
||||
part "message.g.dart";
|
||||
|
||||
typedef EncryptedFile = Map<String, dynamic>;
|
||||
|
||||
@Freezed(unionKey: "msgtype", fallbackUnion: "default")
|
||||
abstract class MessageContent extends Content with _$MessageContent {
|
||||
MessageContent._();
|
||||
|
|
@ -40,7 +38,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
|||
required String body,
|
||||
MessageFormat? format,
|
||||
String? formattedBody,
|
||||
EncryptedFile? file,
|
||||
// EncryptedFile? file
|
||||
String? filename,
|
||||
ImageInfo? info,
|
||||
Uri? url,
|
||||
|
|
@ -51,7 +49,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
|||
required String body,
|
||||
MessageFormat? format,
|
||||
String? formattedBody,
|
||||
EncryptedFile? file,
|
||||
// EncryptedFile? file
|
||||
String? filename,
|
||||
FileInfo? info,
|
||||
Uri? url,
|
||||
|
|
@ -62,7 +60,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
|||
required String body,
|
||||
MessageFormat? format,
|
||||
String? formattedBody,
|
||||
EncryptedFile? file,
|
||||
// EncryptedFile? file
|
||||
String? filename,
|
||||
AudioInfo? info,
|
||||
Uri? url,
|
||||
|
|
@ -73,7 +71,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
|||
required String body,
|
||||
MessageFormat? format,
|
||||
String? formattedBody,
|
||||
EncryptedFile? file,
|
||||
// EncryptedFile? file
|
||||
String? filename,
|
||||
VideoInfo? info,
|
||||
Uri? url,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
import "package:nexus/models/relation_type.dart";
|
||||
part "send_message.freezed.dart";
|
||||
part "send_message.g.dart";
|
||||
|
|
@ -10,7 +9,6 @@ abstract class SendMessageRequest with _$SendMessageRequest {
|
|||
const factory SendMessageRequest({
|
||||
required String roomId,
|
||||
required String text,
|
||||
Content? baseContent,
|
||||
@Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions,
|
||||
@JsonKey(name: "relates_to") Relation? relation,
|
||||
}) = _SendMessageRequest;
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import "package:nexus/controllers/client_id.dart";
|
|||
import "package:nexus/helpers/launch_helper.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/homeserver.dart";
|
||||
import "package:nexus/pages/settings.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/widgets/divider_text.dart";
|
||||
|
||||
|
|
@ -58,9 +57,6 @@ class SelectServerPage extends HookConsumerWidget {
|
|||
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url);
|
||||
|
||||
AppLinks().uriLinkStream.listen((encodedUri) async {
|
||||
final state = encodedUri.queryParameters["state"];
|
||||
if (state != codeResponse.state) return;
|
||||
|
||||
final code = encodedUri.queryParameters["code"];
|
||||
|
||||
if (code != null) {
|
||||
|
|
@ -83,6 +79,9 @@ class SelectServerPage extends HookConsumerWidget {
|
|||
.onError(showError);
|
||||
}
|
||||
});
|
||||
// await Navigator.of(context).push(
|
||||
// MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)),
|
||||
// );
|
||||
}
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
|
|
@ -93,15 +92,7 @@ class SelectServerPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
actions: .new([
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
showDialog(context: context, builder: (_) => SettingsPage()),
|
||||
icon: Icon(Icons.settings),
|
||||
),
|
||||
]),
|
||||
),
|
||||
appBar: Appbar(),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
|
|
|
|||
|
|
@ -127,13 +127,13 @@ class SettingsPage extends ConsumerWidget {
|
|||
),
|
||||
...sections
|
||||
.mapTo(
|
||||
(section, categories) => [
|
||||
(categoryGroup, categories) => [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
).copyWith(bottom: 4),
|
||||
child: DividerText(section),
|
||||
child: DividerText(categoryGroup),
|
||||
),
|
||||
),
|
||||
SliverM3ECardList(
|
||||
|
|
@ -150,14 +150,7 @@ class SettingsPage extends ConsumerWidget {
|
|||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
SettingsCategoryPage(
|
||||
sections
|
||||
.values
|
||||
.flattenedToList
|
||||
.indexOf(
|
||||
categories[index],
|
||||
),
|
||||
),
|
||||
SettingsCategoryPage(index),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import "package:flutter/material.dart";
|
|||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/pages/settings.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/helpers/required_validator_helper.dart";
|
||||
|
||||
|
|
@ -33,15 +32,7 @@ class VerifyPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
actions: .new([
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
showDialog(context: context, builder: (_) => SettingsPage()),
|
||||
icon: Icon(Icons.settings),
|
||||
),
|
||||
]),
|
||||
),
|
||||
appBar: Appbar(),
|
||||
body: AlertDialog(
|
||||
title: Text("Verify"),
|
||||
content: Form(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import "dart:io";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:file_selector/file_selector.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:fluttertagger/fluttertagger.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/models/content/message.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
|
|
@ -15,7 +11,6 @@ import "package:nexus/models/relation_type.dart";
|
|||
import "package:nexus/widgets/composer/mention_overlay.dart";
|
||||
import "package:nexus/widgets/composer/relation_preview.dart";
|
||||
import "package:nexus/widgets/emoji_picker_button.dart";
|
||||
import "package:nexus/main.dart";
|
||||
|
||||
class Composer extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
|
|
@ -56,10 +51,8 @@ class Composer extends HookConsumerWidget {
|
|||
};
|
||||
}
|
||||
|
||||
final attachment = ref.watch(AttachmentController.provider(roomId));
|
||||
|
||||
void send() {
|
||||
if (controller.value.text.isEmpty && attachment == null) return;
|
||||
if (controller.value.text.isEmpty) return;
|
||||
onSend(
|
||||
controller.value.formattedText,
|
||||
shouldMention: shouldMention.value,
|
||||
|
|
@ -77,200 +70,129 @@ class Composer extends HookConsumerWidget {
|
|||
|
||||
return Padding(
|
||||
padding: .all(12),
|
||||
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),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: .all(.circular(12)),
|
||||
child: Column(
|
||||
children: [
|
||||
RelationPreview(
|
||||
relatedEvent,
|
||||
shouldMention: shouldMention.value,
|
||||
toggleShouldMention: () =>
|
||||
shouldMention.value = !shouldMention.value,
|
||||
relationType: relationType,
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: .all(.circular(12)),
|
||||
child: Column(
|
||||
children: [
|
||||
RelationPreview(
|
||||
relatedEvent,
|
||||
shouldMention: shouldMention.value,
|
||||
toggleShouldMention: () =>
|
||||
shouldMention.value = !shouldMention.value,
|
||||
relationType: relationType,
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
Container(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
padding: .symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: .center,
|
||||
children:
|
||||
ref.watch(
|
||||
PowerLevelController.provider(
|
||||
.new(eventType: .message, roomId: roomId),
|
||||
),
|
||||
)
|
||||
? [
|
||||
EmojiPickerButton(
|
||||
context: context,
|
||||
onSelection: (_) => node?.requestFocus(),
|
||||
controller: controller.value,
|
||||
),
|
||||
PopupMenuButton(
|
||||
tooltip: "Add media",
|
||||
enabled: attachment == null,
|
||||
itemBuilder: (context) => [
|
||||
if (Platform.isAndroid || Platform.isIOS)
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text("Camera"),
|
||||
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(
|
||||
child: ListTile(
|
||||
title: Text("Gallery"),
|
||||
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(
|
||||
onTap: () async => ref
|
||||
.watch(
|
||||
AttachmentController.provider(
|
||||
roomId,
|
||||
).notifier,
|
||||
)
|
||||
.add((await openFile())!)
|
||||
.onError(showError),
|
||||
child: ListTile(
|
||||
title: Text("Files"),
|
||||
leading: Icon(Icons.attachment),
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
Expanded(
|
||||
child: FlutterTagger(
|
||||
triggerStrategy: .eager,
|
||||
overlay: MentionOverlay(
|
||||
roomId,
|
||||
query: query.value,
|
||||
triggerCharacter: triggerCharacter.value,
|
||||
addTag: ({required id, required name}) {
|
||||
controller.value.addTag(id: id, name: name);
|
||||
node?.requestFocus();
|
||||
},
|
||||
),
|
||||
controller: controller.value,
|
||||
onSearch: (newQuery, newTriggerCharacter) {
|
||||
triggerCharacter.value = newTriggerCharacter;
|
||||
query.value = newQuery;
|
||||
},
|
||||
triggerCharacterAndStyles: {
|
||||
"@": style,
|
||||
"#": style,
|
||||
},
|
||||
builder: (context, key) => Focus(
|
||||
onKeyEvent: (_, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.enter) {
|
||||
final shiftPressed = HardwareKeyboard
|
||||
.instance
|
||||
.isShiftPressed;
|
||||
|
||||
if (!shiftPressed) {
|
||||
send();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: TextField(
|
||||
maxLines: 12,
|
||||
minLines: 1,
|
||||
autofocus: true,
|
||||
decoration: .new(
|
||||
hintText: "Your message here...",
|
||||
border: .none,
|
||||
),
|
||||
controller: controller.value,
|
||||
key: key,
|
||||
focusNode: node,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
padding: .symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: .center,
|
||||
children:
|
||||
ref.watch(
|
||||
PowerLevelController.provider(
|
||||
.new(eventType: .message, roomId: roomId),
|
||||
),
|
||||
)
|
||||
? [
|
||||
EmojiPickerButton(
|
||||
context: context,
|
||||
onSelection: (_) => node?.requestFocus(),
|
||||
controller: controller.value,
|
||||
),
|
||||
PopupMenuButton(
|
||||
tooltip: "Add media",
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text("Camera"),
|
||||
leading: Icon(Icons.add_a_photo),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed:
|
||||
attachment != null && attachment.$2 == null
|
||||
? null
|
||||
: send,
|
||||
icon: Icon(Icons.send),
|
||||
tooltip: "Send message",
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text("Gallery"),
|
||||
leading: Icon(Icons.add_photo_alternate),
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: .symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
"You don't have permission to send messages in this room...",
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text("Files"),
|
||||
leading: Icon(Icons.attachment),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
Expanded(
|
||||
child: FlutterTagger(
|
||||
triggerStrategy: .eager,
|
||||
overlay: MentionOverlay(
|
||||
roomId,
|
||||
query: query.value,
|
||||
triggerCharacter: triggerCharacter.value,
|
||||
addTag: ({required id, required name}) {
|
||||
controller.value.addTag(id: id, name: name);
|
||||
node?.requestFocus();
|
||||
},
|
||||
),
|
||||
controller: controller.value,
|
||||
onSearch: (newQuery, newTriggerCharacter) {
|
||||
triggerCharacter.value = newTriggerCharacter;
|
||||
query.value = newQuery;
|
||||
},
|
||||
triggerCharacterAndStyles: {"@": style, "#": style},
|
||||
builder: (context, key) => Focus(
|
||||
onKeyEvent: (_, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.enter) {
|
||||
final shiftPressed =
|
||||
HardwareKeyboard.instance.isShiftPressed;
|
||||
|
||||
if (!shiftPressed) {
|
||||
send();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: TextField(
|
||||
maxLines: 12,
|
||||
minLines: 1,
|
||||
autofocus: true,
|
||||
decoration: .new(
|
||||
hintText: "Your message here...",
|
||||
border: .none,
|
||||
),
|
||||
controller: controller.value,
|
||||
key: key,
|
||||
focusNode: node,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: send,
|
||||
icon: Icon(Icons.send),
|
||||
tooltip: "Send message",
|
||||
),
|
||||
]
|
||||
: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: .symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text(
|
||||
"You don't have permission to send messages in this room...",
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,23 +73,20 @@ class MentionOverlay extends ConsumerWidget {
|
|||
:final displayName,
|
||||
:final avatarUrl,
|
||||
) =>
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: ListTile(
|
||||
leading: AvatarOrHash(
|
||||
avatarUrl,
|
||||
displayName ??
|
||||
member.stateKey!.localpart,
|
||||
),
|
||||
title: Text(
|
||||
displayName ??
|
||||
member.stateKey!.localpart,
|
||||
),
|
||||
subtitle: Text(member.stateKey!),
|
||||
onTap: () => addTag(
|
||||
id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})",
|
||||
name: member.stateKey!.localpart,
|
||||
),
|
||||
ListTile(
|
||||
leading: AvatarOrHash(
|
||||
avatarUrl,
|
||||
displayName ??
|
||||
member.stateKey!.localpart,
|
||||
),
|
||||
title: Text(
|
||||
displayName ??
|
||||
member.stateKey!.localpart,
|
||||
),
|
||||
subtitle: Text(member.stateKey!),
|
||||
onTap: () => addTag(
|
||||
id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})",
|
||||
name: member.stateKey!.localpart,
|
||||
),
|
||||
),
|
||||
_ => SizedBox.shrink(),
|
||||
|
|
@ -104,46 +101,40 @@ class MentionOverlay extends ConsumerWidget {
|
|||
? rooms.values
|
||||
: rooms.values.where(
|
||||
(room) =>
|
||||
(room.metadata?.name ??
|
||||
room.metadata?.id ??
|
||||
"")
|
||||
(room.metadata?.name ?? room.metadata!.id)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()),
|
||||
))
|
||||
.map((room) {
|
||||
final name =
|
||||
room.metadata?.name ??
|
||||
room.metadata?.canonicalAlias ??
|
||||
room.metadata?.id ??
|
||||
"Unknown Room";
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: ListTile(
|
||||
leading: AvatarOrHash(
|
||||
room.metadata?.avatar,
|
||||
name,
|
||||
fallback: Icon(Icons.numbers),
|
||||
),
|
||||
title: Text(name),
|
||||
subtitle: room.metadata?.topic == null
|
||||
? null
|
||||
: Text(room.metadata!.topic!, maxLines: 1),
|
||||
onTap: () {
|
||||
final vias = ref.watch(
|
||||
ViaController.provider(room),
|
||||
);
|
||||
addTag(
|
||||
id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)",
|
||||
name:
|
||||
(room.metadata?.canonicalAlias ??
|
||||
room.metadata?.id)
|
||||
?.substring(1)
|
||||
.split(":")
|
||||
.first ??
|
||||
"",
|
||||
);
|
||||
},
|
||||
room.metadata!.canonicalAlias ??
|
||||
room.metadata!.id;
|
||||
return ListTile(
|
||||
leading: AvatarOrHash(
|
||||
room.metadata?.avatar,
|
||||
name,
|
||||
fallback: Icon(Icons.numbers),
|
||||
),
|
||||
title: Text(name),
|
||||
subtitle: room.metadata?.topic == null
|
||||
? null
|
||||
: Text(room.metadata!.topic!, maxLines: 1),
|
||||
onTap: () {
|
||||
final vias = ref.watch(
|
||||
ViaController.provider(room),
|
||||
);
|
||||
addTag(
|
||||
id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)",
|
||||
name:
|
||||
(room.metadata?.canonicalAlias ??
|
||||
room.metadata?.id)
|
||||
?.substring(1)
|
||||
.split(":")
|
||||
.first ??
|
||||
"",
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import Foundation
|
|||
|
||||
import app_links
|
||||
import dynamic_color
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
|
|
@ -20,6 +21,7 @@ import window_manager
|
|||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,5 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,5 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
32
pubspec.lock
32
pubspec.lock
|
|
@ -402,30 +402,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_selector:
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_selector
|
||||
sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a
|
||||
name: file_picker
|
||||
sha256: fdc6a37f715d19f35b131decf1ce39242eeed5ddae18c0818c3eccb731ab76be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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"
|
||||
version: "12.0.0-beta.7"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -450,14 +434,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ dependencies:
|
|||
path_provider: 2.1.6
|
||||
url_launcher: 6.3.2
|
||||
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
|
||||
dynamic_color: 1.8.1
|
||||
collection: 1.19.1
|
||||
|
|
@ -77,7 +78,6 @@ dependencies:
|
|||
xdg_directories: 1.1.0
|
||||
package_info_plus: 10.2.1
|
||||
app_links: 7.2.1
|
||||
file_selector: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.15.1
|
||||
|
|
|
|||
Loading…
Reference in a new issue