feat: Add location sharing

This commit is contained in:
Sorunome 2021-08-01 09:53:43 +02:00 committed by Christian Pauly
commit 5d0967ecda
11 changed files with 425 additions and 1 deletions

View file

@ -28,6 +28,7 @@ import 'package:vrouter/vrouter.dart';
import '../utils/localized_exception_extension.dart';
import 'send_file_dialog.dart';
import 'send_location_dialog.dart';
import 'sticker_picker_dialog.dart';
import '../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
import '../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
@ -351,6 +352,14 @@ class ChatController extends State<Chat> {
);
}
void sendLocationAction() async {
await showDialog(
context: context,
useRootNavigator: false,
builder: (c) => SendLocationDialog(room: room),
);
}
String _getSelectedEventString() {
var copyString = '';
if (selectedEvents.length == 1) {
@ -678,6 +687,9 @@ class ChatController extends State<Chat> {
if (choice == 'voice') {
voiceMessageAction();
}
if (choice == 'location') {
sendLocationAction();
}
}
void onInputBarChanged(String text) {

View file

@ -0,0 +1,145 @@
import 'dart:async';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/cupertino.dart';
import 'package:matrix/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:geolocator/geolocator.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import '../widgets/event_content/map_bubble.dart';
class SendLocationDialog extends StatefulWidget {
final Room room;
const SendLocationDialog({
this.room,
Key key,
}) : super(key: key);
@override
_SendLocationDialogState createState() => _SendLocationDialogState();
}
class _SendLocationDialogState extends State<SendLocationDialog> {
bool disabled = false;
bool denied = false;
bool isSending = false;
Position position;
Error error;
@override
void initState() {
super.initState();
requestLocation();
}
Future<void> requestLocation() async {
if (!(await Geolocator.isLocationServiceEnabled())) {
setState(() => disabled = true);
return;
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
setState(() => denied = true);
return;
}
}
if (permission == LocationPermission.deniedForever) {
setState(() => denied = true);
return;
}
try {
Position _position;
try {
_position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.best,
timeLimit: Duration(seconds: 30),
);
} on TimeoutException {
_position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: Duration(seconds: 30),
);
}
setState(() => position = _position);
} catch (e) {
setState(() => error = e);
}
}
void sendAction() async {
setState(() => isSending = true);
final body =
'https://www.openstreetmap.org/?mlat=${position.latitude}&mlon=${position.longitude}#map=16/${position.latitude}/${position.longitude}';
final uri =
'geo:${position.latitude},${position.longitude};u=${position.accuracy}';
await showFutureLoadingDialog(
context: context,
future: () => widget.room.sendLocation(body, uri),
);
Navigator.of(context, rootNavigator: false).pop();
}
@override
Widget build(BuildContext context) {
Widget contentWidget;
if (position != null) {
contentWidget = MapBubble(
latitude: position.latitude,
longitude: position.longitude,
);
} else if (disabled) {
contentWidget = Text(L10n.of(context).locationDisabledNotice);
} else if (denied) {
contentWidget = Text(L10n.of(context).locationPermissionDeniedNotice);
} else if (error != null) {
contentWidget =
Text(L10n.of(context).errorObtainingLocation(error.toString()));
} else {
contentWidget = Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoActivityIndicator(),
SizedBox(width: 12),
Text(L10n.of(context).obtainingLocation),
],
);
}
if (PlatformInfos.isCupertinoStyle) {
return CupertinoAlertDialog(
title: Text(L10n.of(context).shareLocation),
content: contentWidget,
actions: [
CupertinoDialogAction(
onPressed: Navigator.of(context, rootNavigator: false).pop,
child: Text(L10n.of(context).cancel),
),
CupertinoDialogAction(
onPressed: isSending ? null : sendAction,
child: Text(L10n.of(context).send),
),
],
);
}
return AlertDialog(
title: Text(L10n.of(context).shareLocation),
content: contentWidget,
actions: [
TextButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
child: Text(L10n.of(context).cancel),
),
if (position != null)
TextButton(
onPressed: isSending ? null : sendAction,
child: Text(L10n.of(context).send),
),
],
);
}
}

View file

@ -665,6 +665,21 @@ class ChatView extends StatelessWidget {
contentPadding: EdgeInsets.all(0),
),
),
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'location',
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.brown,
foregroundColor: Colors.white,
child: Icon(
Icons.gps_fixed_outlined),
),
title: Text(L10n.of(context)
.shareLocation),
contentPadding: EdgeInsets.all(0),
),
),
],
),
),

View file

@ -10,6 +10,8 @@ import 'package:vrouter/vrouter.dart';
import 'package:punycode/punycode.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'platform_infos.dart';
class UrlLauncher {
final String url;
final BuildContext context;
@ -30,6 +32,24 @@ class UrlLauncher {
}
if (!{'https', 'http'}.contains(uri.scheme)) {
// just launch non-https / non-http uris directly
// transmute geo URIs on desktop to openstreetmap links, as those usually can't hanlde
// geo URIs
if (!PlatformInfos.isMobile && uri.scheme == 'geo' && uri.path != null) {
final latlong = uri.path
.split(';')
.first
.split(',')
.map((s) => double.tryParse(s))
.toList();
if (latlong.length == 2 &&
latlong.first != null &&
latlong.last != null) {
launch(
'https://www.openstreetmap.org/?mlat=${latlong.first}&mlon=${latlong.last}#map=16/${latlong.first}/${latlong.last}');
return;
}
}
launch(url);
return;
}

View file

@ -0,0 +1,58 @@
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter/material.dart';
class MapBubble extends StatelessWidget {
final double latitude;
final double longitude;
final double zoom;
final double width;
final double height;
final double radius;
const MapBubble({
this.latitude,
this.longitude,
this.zoom = 14.0,
this.width = 400,
this.height = 400,
this.radius = 10.0,
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Container(
constraints: BoxConstraints.loose(Size(width, height)),
child: AspectRatio(
aspectRatio: width / height,
child: FlutterMap(
options: MapOptions(
center: LatLng(latitude, longitude),
zoom: zoom,
),
layers: [
TileLayerOptions(
urlTemplate:
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: ['a', 'b', 'c'],
),
MarkerLayerOptions(
markers: [
Marker(
point: LatLng(latitude, longitude),
builder: (context) => Icon(
Icons.location_pin,
color: Colors.red,
),
),
],
),
],
),
),
),
);
}
}

View file

@ -18,6 +18,7 @@ import '../../config/app_config.dart';
import 'html_message.dart';
import '../matrix.dart';
import 'message_download_content.dart';
import 'map_bubble.dart';
class MessageContent extends StatelessWidget {
final Event event;
@ -164,6 +165,42 @@ class MessageContent extends StatelessWidget {
label: Text(L10n.of(context).encrypted),
);
case MessageTypes.Location:
final geoUri =
Uri.tryParse(event.content.tryGet<String>('geo_uri'));
if (geoUri != null &&
geoUri.scheme == 'geo' &&
geoUri.path != null) {
final latlong = geoUri.path
.split(';')
.first
.split(',')
.map((s) => double.tryParse(s))
.toList();
if (latlong.length == 2 &&
latlong.first != null &&
latlong.last != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
MapBubble(
latitude: latlong.first,
longitude: latlong.last,
),
SizedBox(height: 6),
OutlinedButton.icon(
icon: Icon(Icons.location_on_outlined, color: textColor),
onPressed:
UrlLauncher(context, geoUri.toString()).launchUrl,
label: Text(
L10n.of(context).openInMaps,
style: TextStyle(color: textColor),
),
),
],
);
}
}
continue textmessage;
case MessageTypes.None:
textmessage:
default: