add m.location support

Needs cross-platform testing. Currently only tested on Android.
This commit is contained in:
Henry Hiles 2026-11-02 15:02:49 -05:00
commit 3a8d977f11
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
3 changed files with 810 additions and 12 deletions

View file

@ -4,6 +4,7 @@ import "package:nexus/models/content/content.dart";
import "package:nexus/models/info/file.dart";
import "package:nexus/models/info/image.dart";
import "package:nexus/models/info/video.dart";
import "package:nexus/models/location.dart";
part "message.freezed.dart";
part "message.g.dart";
@ -90,8 +91,12 @@ sealed class MessageContent extends Content with _$MessageContent {
}) = VideoMessageContent;
@FreezedUnionValue("m.location")
factory MessageContent.location({required String body, required Uri geoUri}) =
LocationMessageContent;
factory MessageContent.location({
required String body,
required Uri geoUri,
@JsonKey(name: "m.location", includeIfNull: false) LocationInfo? location,
@JsonKey(name: "m.asset", includeIfNull: false) LocationAsset? asset,
}) = LocationMessageContent;
factory MessageContent.fromJson(Map<String, Object?> json) =>
_$MessageContentFromJson(json);

84
lib/models/location.dart Normal file
View file

@ -0,0 +1,84 @@
import "package:freezed_annotation/freezed_annotation.dart";
import "package:latlong2/latlong.dart";
import "package:nexus/models/content/message.dart";
part "location.freezed.dart";
part "location.g.dart";
/// A location message's `m.location` block.
@Freezed(toJson: false, fromJson: false)
@JsonSerializable(includeIfNull: false)
class const LocationInfo({required final Uri uri, final String? description})
with _$LocationInfo {
Map<String, Object?> toJson() => _$LocationInfoToJson(this);
factory LocationInfo.fromJson(Map<String, Object?> json) =>
_$LocationInfoFromJson(json);
}
/// A location message's `m.asset` block.
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const LocationAsset({
@JsonKey(unknownEnumValue: LocationAssetType.pin)
final LocationAssetType type = .self,
}) with _$LocationAsset {
Map<String, Object?> toJson() => _$LocationAssetToJson(this);
factory LocationAsset.fromJson(Map<String, Object?> json) =>
_$LocationAssetFromJson(json);
}
enum LocationAssetType {
@JsonValue("m.self")
self,
@JsonValue("m.pin")
pin,
}
extension LocationMessagePoint on LocationMessageContent {
LatLng? get point => location?.uri.latLng ?? geoUri.latLng;
String? get description => switch (location?.description?.trim()) {
final description? when description.isNotEmpty => description,
_ => null,
};
LocationAssetType get assetType => asset?.type ?? .self;
}
extension FormatLatLng on LatLng {
String get formatted =>
"${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}";
}
extension GeoUri on Uri {
/// The coordinates of a geo: URI, like `geo:51.5,-0.12;u=35`.
LatLng? get latLng {
if (scheme != "geo") return null;
final coordinates = path.split(";").first.split(",");
if (coordinates.length < 2) return null;
return switch ((
double.tryParse(coordinates[0]),
double.tryParse(coordinates[1]),
)) {
(final latitude?, final longitude?)
when latitude.abs() <= 90 && longitude.abs() <= 180 =>
LatLng(latitude, longitude),
_ => null,
};
}
static Uri openStreetMap(LatLng point) => Uri.https(
"www.openstreetmap.org",
"/",
{"mlat": "${point.latitude}", "mlon": "${point.longitude}"},
).replace(fragment: "map=16/${point.latitude}/${point.longitude}");
static Uri fromLatLng(LatLng point, {double? uncertainty}) => Uri.parse(
"geo:${point.latitude},${point.longitude}"
"${uncertainty == null ? "" : ";u=${uncertainty.round()}"}",
);
}

View file

@ -0,0 +1,11 @@
enum LocationException(final String message) implements Exception {
servicesOff("Location services are turned off."),
denied("Nexus doesn't have permission to see your location."),
timedOut("Couldn't get your location in time."),
failed(
"Couldn't get your location. Check that location services are on, and that Nexus is allowed to use them.",
);
@override
String toString() => message;
}