diff --git a/PRIVACY.md b/PRIVACY.md
index c7797bc69..966f10a3b 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -14,7 +14,7 @@ FluffyChat uses the Matrix protocol. This means that FluffyChat is just a client
For convenience, one or more servers are set as default that the FluffyChat developers consider trustworthy. The developers of FluffyChat do not guarantee their trustworthiness. Before the first communication, users are informed which server they are connecting to.
-FluffyChat only communicates with the selected server and with sentry.io if enabled.
+FluffyChat only communicates with the selected server, with sentry.io if enabled and with [OpenStreetMap](https://openstreetmap.org) to display maps.
More information is available at: [https://matrix.org](https://matrix.org)
@@ -53,6 +53,9 @@ The user is able to save received files and therefore app needs this permission.
#### Read External Storage
The user is able to send files from the device's file system.
+#### Location
+FluffyChat makes it possible to share the current location via the chat. When the user shares their location, FluffyChat uses the device location service and sends the geo-data via Matrix.
+
## Push Notifications
FluffyChat uses the Firebase Cloud Messaging service for push notifications on Android and iOS. This takes place in the following steps:
1. The matrix server sends the push notification to the FluffyChat Push Gateway
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fc5ccdbc9..f95bdb229 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,8 @@
+
+
{
);
}
+ 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 {
if (choice == 'voice') {
voiceMessageAction();
}
+ if (choice == 'location') {
+ sendLocationAction();
+ }
}
void onInputBarChanged(String text) {
diff --git a/lib/pages/send_location_dialog.dart b/lib/pages/send_location_dialog.dart
new file mode 100644
index 000000000..4b83df014
--- /dev/null
+++ b/lib/pages/send_location_dialog.dart
@@ -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 {
+ bool disabled = false;
+ bool denied = false;
+ bool isSending = false;
+ Position position;
+ Error error;
+
+ @override
+ void initState() {
+ super.initState();
+ requestLocation();
+ }
+
+ Future 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),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/views/chat_view.dart b/lib/pages/views/chat_view.dart
index e60f8e2dd..3b718dbfa 100644
--- a/lib/pages/views/chat_view.dart
+++ b/lib/pages/views/chat_view.dart
@@ -665,6 +665,21 @@ class ChatView extends StatelessWidget {
contentPadding: EdgeInsets.all(0),
),
),
+ if (PlatformInfos.isMobile)
+ PopupMenuItem(
+ 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),
+ ),
+ ),
],
),
),
diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart
index 7e2823805..da69fcde7 100644
--- a/lib/utils/url_launcher.dart
+++ b/lib/utils/url_launcher.dart
@@ -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;
}
diff --git a/lib/widgets/event_content/map_bubble.dart b/lib/widgets/event_content/map_bubble.dart
new file mode 100644
index 000000000..2c520386d
--- /dev/null
+++ b/lib/widgets/event_content/map_bubble.dart
@@ -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,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/event_content/message_content.dart b/lib/widgets/event_content/message_content.dart
index a0f2db3a8..a3d1d9f6d 100644
--- a/lib/widgets/event_content/message_content.dart
+++ b/lib/widgets/event_content/message_content.dart
@@ -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('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:
diff --git a/pubspec.lock b/pubspec.lock
index 72445202c..d641eac1e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -426,6 +426,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_map:
+ dependency: "direct main"
+ description:
+ name: flutter_map
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.13.1"
flutter_math_fork:
dependency: transitive
description:
@@ -506,6 +513,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
+ geolocator:
+ dependency: "direct main"
+ description:
+ name: geolocator
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "7.4.0"
+ geolocator_android:
+ dependency: transitive
+ description:
+ name: geolocator_android
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ geolocator_apple:
+ dependency: transitive
+ description:
+ name: geolocator_apple
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ geolocator_platform_interface:
+ dependency: transitive
+ description:
+ name: geolocator_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.3.2"
+ geolocator_web:
+ dependency: transitive
+ description:
+ name: geolocator_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.6"
glob:
dependency: transitive
description:
@@ -620,6 +662,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
+ latlong2:
+ dependency: transitive
+ description:
+ name: latlong2
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.8.0"
+ lists:
+ dependency: transitive
+ description:
+ name: lists
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
localstorage:
dependency: "direct main"
description:
@@ -683,6 +739,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
+ mgrs_dart:
+ dependency: transitive
+ description:
+ name: mgrs_dart
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
mime:
dependency: transitive
description:
@@ -916,6 +979,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
+ positioned_tap_detector_2:
+ dependency: transitive
+ description:
+ name: positioned_tap_detector_2
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
process:
dependency: transitive
description:
@@ -923,6 +993,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
+ proj4dart:
+ dependency: transitive
+ description:
+ name: proj4dart
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
provider:
dependency: "direct main"
description:
@@ -1250,6 +1327,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
+ transparent_image:
+ dependency: transitive
+ description:
+ name: transparent_image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
tuple:
dependency: transitive
description:
@@ -1285,6 +1369,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
+ unicode:
+ dependency: transitive
+ description:
+ name: unicode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.1"
unifiedpush:
dependency: "direct main"
description:
@@ -1418,6 +1509,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
+ wkt_parser:
+ dependency: transitive
+ description:
+ name: wkt_parser
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 8fff87df8..28606fed4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -29,6 +29,7 @@ dependencies:
flutter_local_notifications: ^6.0.0
flutter_localizations:
sdk: flutter
+ flutter_map: ^0.13.1
flutter_matrix_html: ^0.3.0
flutter_olm: ^1.1.2
flutter_openssl_crypto: ^0.0.1
@@ -37,6 +38,7 @@ dependencies:
flutter_svg: ^0.22.0
flutter_typeahead: ^3.2.0
future_loading_dialog: ^0.2.1
+ geolocator: ^7.4.0
hive_flutter: ^1.1.0
image_picker:
git: