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. 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) 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 #### Read External Storage
The user is able to send files from the device's file system. 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"/> ## 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: 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 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.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_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 <application
android:name=".Application" android:name=".Application"
android:label="FluffyChat" android:label="FluffyChat"

View File

@ -833,6 +833,13 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"errorObtainingLocation": "Error obtaining location: {error}",
"@errorObtainingLocation": {
"type": "text",
"placeholders": {
"error": {}
}
},
"directChats": "Direct Chats", "directChats": "Direct Chats",
"@directChats": { "@directChats": {
"type": "text", "type": "text",
@ -1416,6 +1423,11 @@
"number": {} "number": {}
} }
}, },
"obtainingLocation": "Obtaining location…",
"@obtainingLocation": {
"type": "text",
"placeholders": {}
},
"ok": "ok", "ok": "ok",
"@ok": { "@ok": {
"type": "text", "type": "text",
@ -1461,6 +1473,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"openInMaps": "Open in maps",
"@openInMaps": {
"type": "text",
"placeholders": {}
},
"optionalGroupName": "(Optional) Group name", "optionalGroupName": "(Optional) Group name",
"@optionalGroupName": { "@optionalGroupName": {
"type": "text", "type": "text",
@ -1674,6 +1691,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"shareLocation": "Share location",
"@shareLocation": {
"type": "text",
"placeholders": {}
},
"sharedTheLocation": "{username} shared the location", "sharedTheLocation": "{username} shared the location",
"@sharedTheLocation": { "@sharedTheLocation": {
"type": "text", "type": "text",
@ -2089,6 +2111,16 @@
"brand": {} "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": "Single Sign on",
"@singlesignon": { "@singlesignon": {
"type": "text", "type": "text",

View File

@ -28,6 +28,7 @@ import 'package:vrouter/vrouter.dart';
import '../utils/localized_exception_extension.dart'; import '../utils/localized_exception_extension.dart';
import 'send_file_dialog.dart'; import 'send_file_dialog.dart';
import 'send_location_dialog.dart';
import 'sticker_picker_dialog.dart'; import 'sticker_picker_dialog.dart';
import '../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; import '../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
import '../utils/matrix_sdk_extensions.dart/matrix_file_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() { String _getSelectedEventString() {
var copyString = ''; var copyString = '';
if (selectedEvents.length == 1) { if (selectedEvents.length == 1) {
@ -678,6 +687,9 @@ class ChatController extends State<Chat> {
if (choice == 'voice') { if (choice == 'voice') {
voiceMessageAction(); voiceMessageAction();
} }
if (choice == 'location') {
sendLocationAction();
}
} }
void onInputBarChanged(String text) { 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), 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:punycode/punycode.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'platform_infos.dart';
class UrlLauncher { class UrlLauncher {
final String url; final String url;
final BuildContext context; final BuildContext context;
@ -30,6 +32,24 @@ class UrlLauncher {
} }
if (!{'https', 'http'}.contains(uri.scheme)) { if (!{'https', 'http'}.contains(uri.scheme)) {
// just launch non-https / non-http uris directly // 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); launch(url);
return; 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 'html_message.dart';
import '../matrix.dart'; import '../matrix.dart';
import 'message_download_content.dart'; import 'message_download_content.dart';
import 'map_bubble.dart';
class MessageContent extends StatelessWidget { class MessageContent extends StatelessWidget {
final Event event; final Event event;
@ -164,6 +165,42 @@ class MessageContent extends StatelessWidget {
label: Text(L10n.of(context).encrypted), label: Text(L10n.of(context).encrypted),
); );
case MessageTypes.Location: 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: case MessageTypes.None:
textmessage: textmessage:
default: default:

View File

@ -426,6 +426,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_math_fork:
dependency: transitive dependency: transitive
description: description:
@ -506,6 +513,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.1" 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: glob:
dependency: transitive dependency: transitive
description: description:
@ -620,6 +662,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" 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: localstorage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -683,6 +739,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
mgrs_dart:
dependency: transitive
description:
name: mgrs_dart
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -916,6 +979,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" 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: process:
dependency: transitive dependency: transitive
description: description:
@ -923,6 +993,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
proj4dart:
dependency: transitive
description:
name: proj4dart
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1250,6 +1327,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
transparent_image:
dependency: transitive
description:
name: transparent_image
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:
@ -1285,6 +1369,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.1.0"
unicode:
dependency: transitive
description:
name: unicode
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
unifiedpush: unifiedpush:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1418,6 +1509,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -29,6 +29,7 @@ dependencies:
flutter_local_notifications: ^6.0.0 flutter_local_notifications: ^6.0.0
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_map: ^0.13.1
flutter_matrix_html: ^0.3.0 flutter_matrix_html: ^0.3.0
flutter_olm: ^1.1.2 flutter_olm: ^1.1.2
flutter_openssl_crypto: ^0.0.1 flutter_openssl_crypto: ^0.0.1
@ -37,6 +38,7 @@ dependencies:
flutter_svg: ^0.22.0 flutter_svg: ^0.22.0
flutter_typeahead: ^3.2.0 flutter_typeahead: ^3.2.0
future_loading_dialog: ^0.2.1 future_loading_dialog: ^0.2.1
geolocator: ^7.4.0
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
image_picker: image_picker:
git: git: