mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-08 04:59:29 +01:00
Merge branch 'soru/location' into 'main'
feat: Add location sharing Closes #38 See merge request famedly/fluffychat!470
This commit is contained in:
commit
340dc631eb
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
145
lib/pages/send_location_dialog.dart
Normal file
145
lib/pages/send_location_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
|
58
lib/widgets/event_content/map_bubble.dart
Normal file
58
lib/widgets/event_content/map_bubble.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
98
pubspec.lock
98
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:
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user