Merge branch 'soru/location' into 'main'

feat: Add location sharing

Closes #38

See merge request famedly/fluffychat!470
This commit is contained in:
Krille Fear 2021-08-06 11:14:54 +00:00
commit 340dc631eb
11 changed files with 425 additions and 1 deletions

View File

@ -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<a id="6"/>
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

View File

@ -10,6 +10,8 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name=".Application"
android:label="FluffyChat"

View File

@ -833,6 +833,13 @@
"type": "text",
"placeholders": {}
},
"errorObtainingLocation": "Error obtaining location: {error}",
"@errorObtainingLocation": {
"type": "text",
"placeholders": {
"error": {}
}
},
"directChats": "Direct Chats",
"@directChats": {
"type": "text",
@ -1416,6 +1423,11 @@
"number": {}
}
},
"obtainingLocation": "Obtaining location…",
"@obtainingLocation": {
"type": "text",
"placeholders": {}
},
"ok": "ok",
"@ok": {
"type": "text",
@ -1461,6 +1473,11 @@
"type": "text",
"placeholders": {}
},
"openInMaps": "Open in maps",
"@openInMaps": {
"type": "text",
"placeholders": {}
},
"optionalGroupName": "(Optional) Group name",
"@optionalGroupName": {
"type": "text",
@ -1674,6 +1691,11 @@
"type": "text",
"placeholders": {}
},
"shareLocation": "Share location",
"@shareLocation": {
"type": "text",
"placeholders": {}
},
"sharedTheLocation": "{username} shared the location",
"@sharedTheLocation": {
"type": "text",
@ -2089,6 +2111,16 @@
"brand": {}
}
},
"locationDisabledNotice": "Location services are disabled. Please enable them to be able to share your location.",
"@locationDisabledNotice": {
"type": "text",
"placeholders": {}
},
"locationPermissionDeniedNotice": "Location permission denied. Please grant them to be able to share your location.",
"@locationPermissionDeniedNotice": {
"type": "text",
"placeholders": {}
},
"singlesignon": "Single Sign on",
"@singlesignon": {
"type": "text",

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:

View File

@ -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:

View File

@ -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: