forked from Nexus/nexus
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
716541b457 |
|||
|
965c7ea2dc |
|||
|
85d02f2ac9 |
|||
|
1c446c4cdf |
|||
|
51430d2830 |
|||
|
4e208764f5 |
|||
|
eecc307f04 |
|||
|
6221eb4a7a |
|||
|
370549072e |
|||
|
80b938c0be |
|||
|
87e1ef0d9b |
|||
|
f8c705531c |
|||
|
2a91959333 |
|||
|
34ddd6d32b |
|||
|
a8726feef6 |
|||
|
bd1bad76d7 |
|||
|
c4f63d18b5 |
|||
|
a8285548e8 |
|||
|
cdad9b760e |
|||
|
06c5411dfc |
|||
|
f6b5bd4057 |
|||
|
0da3b4da22 |
25 changed files with 493 additions and 239 deletions
|
|
@ -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
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
BIN
assets/twim/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
2
gomuks
2
gomuks
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3c759ba03a5235deb82cb1559ebc42c36ee0e874
|
Subproject commit c06eb1cbda2d49ff199a351e2d55df6ff7ab602b
|
||||||
40
lib/controllers/attachment.dart
Normal file
40
lib/controllers/attachment.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
11
lib/controllers/image_picker.dart
Normal file
11
lib/controllers/image_picker.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
24
lib/models/requests/upload_media.dart
Normal file
24
lib/models/requests/upload_media.dart
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
32
pubspec.lock
32
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue