forked from Nexus/nexus
Compare commits
24 changed files with 249 additions and 175 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
[submodule "gomuks"]
|
[submodule "gomuks"]
|
||||||
path = gomuks
|
path = gomuks
|
||||||
url = https://github.com/henry-hiles/gomuks
|
url = https://github.com/gomuks/gomuks
|
||||||
branch = main
|
branch = main
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] Edits
|
- [x] Edits
|
||||||
- [x] Attachments
|
- [x] Attachments
|
||||||
- [x] Unencrypted
|
- [x] Unencrypted
|
||||||
- [x] Encrypted
|
- [ ] Encrypted
|
||||||
- [x] Blurhashing
|
- [x] Blurhashing
|
||||||
- [ ] Downloading attachments
|
- [ ] Downloading attachments
|
||||||
- [x] Opening attachments in their own view
|
- [x] Opening attachments in their own view
|
||||||
|
|
|
||||||
2
gomuks
2
gomuks
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1f11743884a64765b1444b250377a09ebcaba93c
|
Subproject commit c06eb1cbda2d49ff199a351e2d55df6ff7ab602b
|
||||||
|
|
@ -18,7 +18,6 @@ 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";
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
|
||||||
import "package:nexus/models/requests/get_event.dart";
|
import "package:nexus/models/requests/get_event.dart";
|
||||||
import "package:nexus/models/requests/get_related_events.dart";
|
import "package:nexus/models/requests/get_related_events.dart";
|
||||||
import "package:nexus/models/requests/get_room_state.dart";
|
import "package:nexus/models/requests/get_room_state.dart";
|
||||||
|
|
@ -272,9 +271,6 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
||||||
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
||||||
|
|
||||||
Future<File> downloadMedia(DownloadMediaRequest request) async =>
|
|
||||||
.new((await _sendCommand("download_media", request.toJson()))["path"]);
|
|
||||||
|
|
||||||
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/cross_cache.dart
Normal file
11
lib/controllers/cross_cache.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
|
class CrossCacheController extends Notifier<CrossCache> {
|
||||||
|
@override
|
||||||
|
CrossCache build() => .new();
|
||||||
|
|
||||||
|
static final provider = NotifierProvider<CrossCacheController, CrossCache>(
|
||||||
|
CrossCacheController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -121,16 +121,9 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
required RelationType relationType,
|
required RelationType relationType,
|
||||||
Event? relation,
|
Event? relation,
|
||||||
}) async {
|
}) async {
|
||||||
Content? baseContent;
|
final attachment = ref.watch(AttachmentController.provider(roomId));
|
||||||
if (relationType == .edit) {
|
|
||||||
baseContent = relation?.content;
|
|
||||||
} else {
|
|
||||||
final attachment = ref.watch(AttachmentController.provider(roomId));
|
|
||||||
if (attachment != null && attachment.$2 == null) return;
|
|
||||||
|
|
||||||
baseContent = attachment?.$2;
|
if (attachment != null && attachment.$2 == null) return;
|
||||||
ref.invalidate(AttachmentController.provider(roomId));
|
|
||||||
}
|
|
||||||
|
|
||||||
var taggedMessage = text;
|
var taggedMessage = text;
|
||||||
|
|
||||||
|
|
@ -148,7 +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: baseContent,
|
baseContent: attachment?.$2,
|
||||||
mentions: Mentions(
|
mentions: Mentions(
|
||||||
userIds: [
|
userIds: [
|
||||||
if (shouldMention == true &&
|
if (shouldMention == true &&
|
||||||
|
|
@ -165,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(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:http/http.dart";
|
import "package:http/http.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/header.dart";
|
import "package:nexus/controllers/header.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/models/open_graph_data.dart";
|
import "package:nexus/models/open_graph_data.dart";
|
||||||
|
|
||||||
class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
||||||
|
|
@ -29,9 +30,12 @@ class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
||||||
final decodedValue = json.decode(response.body);
|
final decodedValue = json.decode(response.body);
|
||||||
if (decodedValue is! Map<String, dynamic>) return null;
|
if (decodedValue is! Map<String, dynamic>) return null;
|
||||||
|
|
||||||
return .fromJson(
|
final mxc = decodedValue["og:image"];
|
||||||
decodedValue,
|
final image = mxc == null
|
||||||
).copyWith(imageUrl: decodedValue["og:image"]);
|
? null
|
||||||
|
: Uri.tryParse(mxc)?.mxcToHttps(homeserver);
|
||||||
|
|
||||||
|
return .fromJson(decodedValue).copyWith(imageUrl: image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
lib/helpers/extensions/mxc_to_https.dart
Normal file
4
lib/helpers/extensions/mxc_to_https.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
extension MxcToHttps on Uri {
|
||||||
|
Uri mxcToHttps(String homeserver) =>
|
||||||
|
.parse(homeserver).resolve("_matrix/client/v1/media/download/$host$path");
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import "dart:ui";
|
|
||||||
import "package:flutter/widgets.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/client.dart";
|
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
|
||||||
|
|
||||||
class MxcImage extends ImageProvider<MxcImage> {
|
|
||||||
final WidgetRef ref;
|
|
||||||
final DownloadMediaRequest request;
|
|
||||||
const MxcImage(this.ref, this.request);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<MxcImage> obtainKey(ImageConfiguration configuration) =>
|
|
||||||
Future.value(this);
|
|
||||||
|
|
||||||
@override
|
|
||||||
ImageStreamCompleter loadImage(MxcImage key, ImageDecoderCallback decode) =>
|
|
||||||
MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1.0);
|
|
||||||
|
|
||||||
Future<Codec> _loadAsync(ImageDecoderCallback decode) async {
|
|
||||||
final file = await ref
|
|
||||||
.read(ClientController.provider.notifier)
|
|
||||||
.downloadMedia(request);
|
|
||||||
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
|
||||||
|
|
||||||
return decode(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is MxcImage && other.request == request;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => request.hashCode;
|
|
||||||
}
|
|
||||||
|
|
@ -12,9 +12,6 @@ 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._();
|
||||||
static String? mediaUrlFromJson(Map<dynamic, dynamic> json, String key) =>
|
|
||||||
json[key] ?? json["file"]?[key];
|
|
||||||
|
|
||||||
factory MessageContent({String? body}) = UnknownMessageContent;
|
factory MessageContent({String? body}) = UnknownMessageContent;
|
||||||
|
|
||||||
@FreezedUnionValue("m.text")
|
@FreezedUnionValue("m.text")
|
||||||
|
|
@ -46,7 +43,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
||||||
EncryptedFile? file,
|
EncryptedFile? file,
|
||||||
String? filename,
|
String? filename,
|
||||||
ImageInfo? info,
|
ImageInfo? info,
|
||||||
@JsonKey(readValue: MessageContent.mediaUrlFromJson) required Uri url,
|
Uri? url,
|
||||||
}) = ImageMessageContent;
|
}) = ImageMessageContent;
|
||||||
|
|
||||||
@FreezedUnionValue("m.file")
|
@FreezedUnionValue("m.file")
|
||||||
|
|
@ -57,7 +54,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
||||||
EncryptedFile? file,
|
EncryptedFile? file,
|
||||||
String? filename,
|
String? filename,
|
||||||
FileInfo? info,
|
FileInfo? info,
|
||||||
@JsonKey(readValue: MessageContent.mediaUrlFromJson) required Uri url,
|
Uri? url,
|
||||||
}) = FileMessageContent;
|
}) = FileMessageContent;
|
||||||
|
|
||||||
@FreezedUnionValue("m.audio")
|
@FreezedUnionValue("m.audio")
|
||||||
|
|
@ -68,7 +65,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
||||||
EncryptedFile? file,
|
EncryptedFile? file,
|
||||||
String? filename,
|
String? filename,
|
||||||
AudioInfo? info,
|
AudioInfo? info,
|
||||||
@JsonKey(readValue: MessageContent.mediaUrlFromJson) required Uri url,
|
Uri? url,
|
||||||
}) = AudioMessageContent;
|
}) = AudioMessageContent;
|
||||||
|
|
||||||
@FreezedUnionValue("m.video")
|
@FreezedUnionValue("m.video")
|
||||||
|
|
@ -79,7 +76,7 @@ abstract class MessageContent extends Content with _$MessageContent {
|
||||||
EncryptedFile? file,
|
EncryptedFile? file,
|
||||||
String? filename,
|
String? filename,
|
||||||
VideoInfo? info,
|
VideoInfo? info,
|
||||||
@JsonKey(readValue: MessageContent.mediaUrlFromJson) required Uri url,
|
Uri? url,
|
||||||
}) = VideoMessageContent;
|
}) = VideoMessageContent;
|
||||||
|
|
||||||
@FreezedUnionValue("m.location")
|
@FreezedUnionValue("m.location")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
|
||||||
part "download_media.freezed.dart";
|
|
||||||
part "download_media.g.dart";
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class DownloadMediaRequest with _$DownloadMediaRequest {
|
|
||||||
const factory DownloadMediaRequest({
|
|
||||||
required Uri mxc,
|
|
||||||
@Default(false) bool encrypted,
|
|
||||||
@Default(false) bool isAvatar,
|
|
||||||
@Default(false) bool thumbnailAvatar,
|
|
||||||
}) = _DownloadMediaRequest;
|
|
||||||
|
|
||||||
factory DownloadMediaRequest.fromJson(Map<String, Object?> json) =>
|
|
||||||
_$DownloadMediaRequestFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
@ -16,9 +16,6 @@ abstract class OAuthRegisterClientRequest with _$OAuthRegisterClientRequest {
|
||||||
IList<GrantType>? grantTypes,
|
IList<GrantType>? grantTypes,
|
||||||
IList<Uri>? redirectUris,
|
IList<Uri>? redirectUris,
|
||||||
IList<ResponseType>? responseTypes,
|
IList<ResponseType>? responseTypes,
|
||||||
|
|
||||||
@Default(AuthMethod.none)
|
|
||||||
@JsonKey(name: "token_endpoint_auth_method")
|
|
||||||
AuthMethod? authMethod,
|
AuthMethod? authMethod,
|
||||||
}) = _OAuthRegisterClientRequest;
|
}) = _OAuthRegisterClientRequest;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import "package:color_hash/color_hash.dart";
|
import "package:color_hash/color_hash.dart";
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/controllers/cross_cache.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
|
|
||||||
class AvatarOrHash extends ConsumerWidget {
|
class AvatarOrHash extends ConsumerWidget {
|
||||||
final Uri? avatar;
|
final Uri? avatar;
|
||||||
|
|
@ -23,6 +27,15 @@ class AvatarOrHash extends ConsumerWidget {
|
||||||
child: Center(child: Icon(Icons.person, size: height / 2)),
|
child: Center(child: Icon(Icons.person, size: height / 2)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final parsedAvatar = avatar?.mxcToHttps(
|
||||||
|
ref.watch(
|
||||||
|
ClientStateController.provider.select(
|
||||||
|
(value) => value?.homeserverUrl,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: height,
|
width: height,
|
||||||
height: height,
|
height: height,
|
||||||
|
|
@ -32,10 +45,14 @@ class AvatarOrHash extends ConsumerWidget {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: height,
|
width: height,
|
||||||
height: height,
|
height: height,
|
||||||
child: avatar == null
|
child: parsedAvatar == null
|
||||||
? fallback ?? box
|
? fallback ?? box
|
||||||
: Image(
|
: Image(
|
||||||
image: MxcImage(ref, .new(mxc: avatar!, isAvatar: true)),
|
image: CachedNetworkImage(
|
||||||
|
parsedAvatar.toString(),
|
||||||
|
ref.watch(CrossCacheController.provider),
|
||||||
|
headers: ref.headers,
|
||||||
|
),
|
||||||
fit: .cover,
|
fit: .cover,
|
||||||
loadingBuilder: (_, child, loadingProgress) =>
|
loadingBuilder: (_, child, loadingProgress) =>
|
||||||
loadingProgress == null ? child : fallback ?? box,
|
loadingProgress == null ? child : fallback ?? box,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:m3e_buttons/m3e_buttons.dart";
|
import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
import "package:nexus/controllers/cross_cache.dart";
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
|
||||||
class ExpandableImage extends ConsumerWidget {
|
class ExpandableImage extends ConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final DownloadMediaRequest? request;
|
final String? source;
|
||||||
const ExpandableImage(this.request, {required this.child, super.key});
|
const ExpandableImage(this.source, {required this.child, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => InkWell(
|
Widget build(BuildContext context, WidgetRef ref) => InkWell(
|
||||||
onTap: request == null
|
onTap: source == null
|
||||||
? null
|
? null
|
||||||
: () => showDialog(
|
: () => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -26,10 +27,14 @@ class ExpandableImage extends ConsumerWidget {
|
||||||
maxScale: 10,
|
maxScale: 10,
|
||||||
child: Image(
|
child: Image(
|
||||||
errorBuilder: (_, error, stackTrace) => ErrorDialog(
|
errorBuilder: (_, error, stackTrace) => ErrorDialog(
|
||||||
"Loading failed for ${request?.mxc}\nError: $error",
|
"Loading failed for $source\nError: $error",
|
||||||
stackTrace,
|
stackTrace,
|
||||||
),
|
),
|
||||||
image: MxcImage(ref, request!),
|
image: CachedNetworkImage(
|
||||||
|
source!,
|
||||||
|
ref.watch(CrossCacheController.provider),
|
||||||
|
headers: ref.headers,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
||||||
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/controllers/cross_cache.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
|
||||||
import "package:nexus/widgets/expandable_image.dart";
|
import "package:nexus/widgets/expandable_image.dart";
|
||||||
import "package:nexus/widgets/html/mention_chip.dart";
|
import "package:nexus/widgets/html/mention_chip.dart";
|
||||||
import "package:nexus/widgets/html/spoiler_text.dart";
|
import "package:nexus/widgets/html/spoiler_text.dart";
|
||||||
|
|
@ -36,7 +40,16 @@ class Html extends ConsumerWidget {
|
||||||
(element.attributes.keys.contains("data-mx-emoticon") ? 32 : null) ??
|
(element.attributes.keys.contains("data-mx-emoticon") ? 32 : null) ??
|
||||||
300;
|
300;
|
||||||
final width = int.tryParse(element.attributes["width"] ?? "");
|
final width = int.tryParse(element.attributes["width"] ?? "");
|
||||||
final src = Uri.tryParse(element.attributes["src"] ?? "");
|
final src = Uri.tryParse(element.attributes["src"] ?? "")
|
||||||
|
?.mxcToHttps(
|
||||||
|
ref.watch(
|
||||||
|
ClientStateController.provider.select(
|
||||||
|
(value) => value?.homeserverUrl,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.toString();
|
||||||
|
|
||||||
return switch (element.localName) {
|
return switch (element.localName) {
|
||||||
"code" =>
|
"code" =>
|
||||||
|
|
@ -64,9 +77,13 @@ class Html extends ConsumerWidget {
|
||||||
: InlineCustomWidget(
|
: InlineCustomWidget(
|
||||||
alignment: PlaceholderAlignment.middle,
|
alignment: PlaceholderAlignment.middle,
|
||||||
child: ExpandableImage(
|
child: ExpandableImage(
|
||||||
.new(mxc: src),
|
src,
|
||||||
child: Image(
|
child: Image(
|
||||||
image: MxcImage(ref, .new(mxc: src)),
|
image: CachedNetworkImage(
|
||||||
|
src,
|
||||||
|
ref.watch(CrossCacheController.provider),
|
||||||
|
headers: ref.headers,
|
||||||
|
),
|
||||||
errorBuilder: (_, error, _) => Text(
|
errorBuilder: (_, error, _) => Text(
|
||||||
"Image Failed to Load",
|
"Image Failed to Load",
|
||||||
style: .new(color: Theme.of(context).colorScheme.error),
|
style: .new(color: Theme.of(context).colorScheme.error),
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,57 @@
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_blurhash/flutter_blurhash.dart";
|
import "package:flutter_blurhash/flutter_blurhash.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
import "package:nexus/controllers/cross_cache.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/models/info/image.dart" as i;
|
import "package:nexus/models/info/image.dart" as i;
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
|
||||||
import "package:nexus/widgets/expandable_image.dart";
|
import "package:nexus/widgets/expandable_image.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
||||||
class MessageImage extends ConsumerWidget {
|
class MessageImage extends ConsumerWidget {
|
||||||
final Uri url;
|
final Uri url;
|
||||||
final i.ImageInfo? info;
|
final i.ImageInfo? info;
|
||||||
final bool encrypted;
|
const MessageImage(this.url, {this.info, super.key});
|
||||||
const MessageImage(this.url, {this.info, required this.encrypted, super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) => ExpandableImage(
|
||||||
final request = DownloadMediaRequest(mxc: url, encrypted: encrypted);
|
url.toString(),
|
||||||
return ExpandableImage(
|
child: ClipRRect(
|
||||||
request,
|
borderRadius: .all(.circular(8)),
|
||||||
child: ClipRRect(
|
child: AspectRatio(
|
||||||
borderRadius: .all(.circular(8)),
|
aspectRatio: (info?.width ?? 1) / (info?.height ?? 1),
|
||||||
child: AspectRatio(
|
child: Image(
|
||||||
aspectRatio: (info?.width ?? 1) / (info?.height ?? 1),
|
image: CachedNetworkImage(
|
||||||
child: Image(
|
url.toString(),
|
||||||
image: MxcImage(ref, request),
|
ref.watch(CrossCacheController.provider),
|
||||||
width: info?.width,
|
headers: ref.headers,
|
||||||
fit: BoxFit.fitWidth,
|
),
|
||||||
loadingBuilder: (_, child, loadingProgress) =>
|
width: info?.width,
|
||||||
loadingProgress == null
|
fit: BoxFit.fitWidth,
|
||||||
? child
|
loadingBuilder: (_, child, loadingProgress) => loadingProgress == null
|
||||||
: switch (info?.blurHash) {
|
? child
|
||||||
final blurHash? =>
|
: switch (info?.blurHash) {
|
||||||
info?.width == null || info?.height == null
|
final blurHash? =>
|
||||||
? SizedBox(
|
info?.width == null || info?.height == null
|
||||||
width: 200,
|
? SizedBox(
|
||||||
height: 200,
|
width: 200,
|
||||||
child: BlurHash(hash: blurHash),
|
height: 200,
|
||||||
)
|
child: BlurHash(hash: blurHash),
|
||||||
: SizedBox(
|
)
|
||||||
width: info!.width,
|
: SizedBox(
|
||||||
child: BlurHash(hash: blurHash),
|
width: info!.width,
|
||||||
),
|
child: BlurHash(hash: blurHash),
|
||||||
_ => Loading(),
|
),
|
||||||
},
|
_ => Loading(),
|
||||||
errorBuilder: (context, error, stackTrace) => Center(
|
},
|
||||||
child: Text(
|
errorBuilder: (context, error, stackTrace) => Center(
|
||||||
"Image Failed to Load",
|
child: Text(
|
||||||
style: .new(color: Theme.of(context).colorScheme.error),
|
"Image Failed to Load",
|
||||||
),
|
style: .new(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/controllers/cross_cache.dart";
|
||||||
import "package:nexus/controllers/reactions.dart";
|
import "package:nexus/controllers/reactions.dart";
|
||||||
import "package:nexus/controllers/room_chat.dart";
|
import "package:nexus/controllers/room_chat.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -52,9 +55,16 @@ class ReactionRow extends ConsumerWidget {
|
||||||
child: reaction.startsWith("mxc://")
|
child: reaction.startsWith("mxc://")
|
||||||
? Image(
|
? Image(
|
||||||
height: 20,
|
height: 20,
|
||||||
image: MxcImage(
|
image: CachedNetworkImage(
|
||||||
ref,
|
headers: ref.headers,
|
||||||
.new(mxc: Uri.parse(reaction)),
|
Uri.parse(reaction)
|
||||||
|
.mxcToHttps(
|
||||||
|
clientState!.homeserverUrl!,
|
||||||
|
)
|
||||||
|
.toString(),
|
||||||
|
ref.watch(
|
||||||
|
CrossCacheController.provider,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(reaction, overflow: .ellipsis),
|
: Text(reaction, overflow: .ellipsis),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:linkify/linkify.dart";
|
import "package:linkify/linkify.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/event.dart";
|
import "package:nexus/controllers/event.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/models/content/encrypted.dart";
|
import "package:nexus/models/content/encrypted.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/content/sticker.dart";
|
import "package:nexus/models/content/sticker.dart";
|
||||||
|
|
@ -145,9 +146,14 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
: ConstrainedBox(
|
: ConstrainedBox(
|
||||||
constraints: .loose(.square(200)),
|
constraints: .loose(.square(200)),
|
||||||
child: MessageImage(
|
child: MessageImage(
|
||||||
url,
|
url.mxcToHttps(
|
||||||
|
ref.watch(
|
||||||
|
ClientStateController.provider.select(
|
||||||
|
(value) => value!.homeserverUrl!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
info: info,
|
info: info,
|
||||||
encrypted: false,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// TODO: Handle locations
|
// TODO: Handle locations
|
||||||
|
|
@ -223,30 +229,35 @@ class MessageRenderer extends ConsumerWidget {
|
||||||
FileMessageContent(:final url) ||
|
FileMessageContent(:final url) ||
|
||||||
VideoMessageContent(:final url) ||
|
VideoMessageContent(:final url) ||
|
||||||
AudioMessageContent(:final url))
|
AudioMessageContent(:final url))
|
||||||
ConstrainedBox(
|
switch (url?.mxcToHttps(
|
||||||
constraints: .loose(.square(500)),
|
ref.watch(
|
||||||
child: switch (event.content) {
|
ClientStateController.provider.select(
|
||||||
VideoMessageContent(:final info) =>
|
(value) => value!.homeserverUrl!,
|
||||||
VideoPlayer(url, info),
|
),
|
||||||
AudioMessageContent(:final info) =>
|
),
|
||||||
AudioPlayer(url, info),
|
)) {
|
||||||
FileMessageContent(
|
final url? => ConstrainedBox(
|
||||||
:final info,
|
constraints: .loose(.square(500)),
|
||||||
:final filename,
|
child: switch (event.content) {
|
||||||
) =>
|
VideoMessageContent(:final info) =>
|
||||||
FileCard(url, info, filename: filename),
|
VideoPlayer(url, info),
|
||||||
ImageMessageContent(
|
AudioMessageContent(:final info) =>
|
||||||
:final info,
|
AudioPlayer(url, info),
|
||||||
:final file,
|
FileMessageContent(
|
||||||
) =>
|
:final info,
|
||||||
MessageImage(
|
:final filename,
|
||||||
url,
|
) =>
|
||||||
info: info,
|
FileCard(url, info, filename: filename),
|
||||||
encrypted: file != null,
|
ImageMessageContent(:final info) =>
|
||||||
),
|
MessageImage(url, info: info),
|
||||||
_ => SizedBox.shrink(),
|
_ => SizedBox.shrink(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_ => Text(
|
||||||
|
"Nexus currently cannot handle encrypted media",
|
||||||
|
style: errorStyle,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
if (event.lastEditRowId != 0)
|
if (event.lastEditRowId != 0)
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/widgets/appbar.dart";
|
import "package:nexus/widgets/appbar.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/widgets/expandable_image.dart";
|
import "package:nexus/widgets/expandable_image.dart";
|
||||||
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/widgets/linkified_text.dart";
|
import "package:nexus/widgets/linkified_text.dart";
|
||||||
import "package:nexus/widgets/room_menu.dart";
|
import "package:nexus/widgets/room_menu.dart";
|
||||||
|
|
||||||
|
|
@ -51,9 +53,15 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
children: [
|
children: [
|
||||||
if (room.metadata?.avatar != null)
|
if (room.metadata?.avatar != null)
|
||||||
ExpandableImage(
|
ExpandableImage(
|
||||||
room.metadata?.avatar == null
|
room.metadata!.avatar!
|
||||||
? null
|
.mxcToHttps(
|
||||||
: .new(mxc: room.metadata!.avatar!),
|
ref.watch(
|
||||||
|
ClientStateController.provider.select(
|
||||||
|
(value) => value!.homeserverUrl!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toString(),
|
||||||
child: AvatarOrHash(
|
child: AvatarOrHash(
|
||||||
room.metadata?.avatar,
|
room.metadata?.avatar,
|
||||||
room.metadata?.name ?? "Unnamed Room",
|
room.metadata?.name ?? "Unnamed Room",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/cross_cache.dart";
|
||||||
import "package:nexus/controllers/url_preview.dart";
|
import "package:nexus/controllers/url_preview.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:nexus/helpers/mxc_image.dart";
|
|
||||||
|
|
||||||
class UrlPreview extends ConsumerWidget {
|
class UrlPreview extends ConsumerWidget {
|
||||||
final String link;
|
final String link;
|
||||||
|
|
@ -46,9 +48,10 @@ class UrlPreview extends ConsumerWidget {
|
||||||
child: Image(
|
child: Image(
|
||||||
errorBuilder: (_, _, _) => SizedBox.shrink(),
|
errorBuilder: (_, _, _) => SizedBox.shrink(),
|
||||||
width: preview.width,
|
width: preview.width,
|
||||||
image: MxcImage(
|
image: CachedNetworkImage(
|
||||||
ref,
|
preview.imageUrl.toString(),
|
||||||
.new(mxc: preview.imageUrl!),
|
ref.watch(CrossCacheController.provider),
|
||||||
|
headers: ref.headers,
|
||||||
),
|
),
|
||||||
fit: .fitWidth,
|
fit: .fitWidth,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import "package:nexus/controllers/power_level.dart";
|
||||||
import "package:nexus/controllers/profile.dart";
|
import "package:nexus/controllers/profile.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/membership_action.dart";
|
import "package:nexus/models/membership_action.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
@ -96,7 +97,15 @@ class UserBottomSheet extends ConsumerWidget {
|
||||||
SizedBox(height: 18),
|
SizedBox(height: 18),
|
||||||
|
|
||||||
ExpandableImage(
|
ExpandableImage(
|
||||||
member.avatarUrl == null ? null : .new(mxc: member.avatarUrl!),
|
member.avatarUrl
|
||||||
|
?.mxcToHttps(
|
||||||
|
ref.watch(
|
||||||
|
ClientStateController.provider.select(
|
||||||
|
(value) => value!.homeserverUrl!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toString(),
|
||||||
child: AvatarOrHash(
|
child: AvatarOrHash(
|
||||||
member.avatarUrl,
|
member.avatarUrl,
|
||||||
member.displayName ?? userId.localpart,
|
member.displayName ?? userId.localpart,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ buildGoModule (finalAttrs: {
|
||||||
|
|
||||||
src = "${src}/gomuks";
|
src = "${src}/gomuks";
|
||||||
|
|
||||||
vendorHash = "sha256-4QU51WGiNKYm7z/ajnas2qQaMK5BgyHwPfmNpQvW0qg=";
|
vendorHash = "sha256-mNZEHOGByVqK1kSLC/Cf1VEvkDxRen+TmoV5CXvGrZ4=";
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
|
|
|
||||||
40
pubspec.lock
40
pubspec.lock
|
|
@ -249,6 +249,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.1"
|
version: "1.15.1"
|
||||||
|
cross_cache:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cross_cache
|
||||||
|
sha256: "4983a16603cc99b0a14de6a772fa8ee4533411f46f3c423f1386fea7566049c5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -289,6 +297,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.14"
|
version: "0.7.14"
|
||||||
|
dio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.10.0"
|
||||||
|
dio_web_adapter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio_web_adapter
|
||||||
|
sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -662,6 +686,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
version: "0.2.1"
|
||||||
|
idb_shim:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: idb_shim
|
||||||
|
sha256: "3448298f244bc76a14aca71461eeab6add2b8a877930521c42c2e8b71b644590"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.6+2"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1216,6 +1248,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
sembast:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sembast
|
||||||
|
sha256: a58b26925e23071cf0f4754d8449aabe829e9a9930a872fb18e6ae4c2c88e025
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.8.9+1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ dependencies:
|
||||||
fluttertagger: 2.3.2
|
fluttertagger: 2.3.2
|
||||||
dynamic_polls: 0.0.7
|
dynamic_polls: 0.0.7
|
||||||
flutter_hooks: 0.21.3+1
|
flutter_hooks: 0.21.3+1
|
||||||
|
cross_cache: 1.1.0
|
||||||
ffi: 2.2.0
|
ffi: 2.2.0
|
||||||
hooks: 2.0.2
|
hooks: 2.0.2
|
||||||
code_assets: 1.2.1
|
code_assets: 1.2.1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue