Merge branch 'soru/send-files' into 'master'

feat: Send file dialog

Closes #84, #112, and #140

See merge request ChristianPauly/fluffychat-flutter!150
This commit is contained in:
Sorunome 2020-09-09 11:53:45 +00:00
commit fadf8715c3
11 changed files with 245 additions and 45 deletions

View file

@ -2,12 +2,14 @@
### Features ### Features
- Added translations: Armenian, Turkish, Chinese (Simplified) - Added translations: Armenian, Turkish, Chinese (Simplified)
- Url-ify matrix identifiers - Url-ify matrix identifiers
- Use server-side generated thumbnails in cleartext rooms
- Add option to send images in their original resolution
- Add additional confirmation for sending files & share intents
### Changes ### Changes
- Tapping links, pills, etc. now does stuff - Tapping links, pills, etc. now does stuff
### Fixes: ### Fixes:
- Various html rendering and url-ifying fixes - Various html rendering and url-ifying fixes
- Added support for blurhashes - Added support for blurhashes
- Use server-side generated thumbnails in cleartext rooms
- Image viewer now eventually displays the original image, not only the thumbnail - Image viewer now eventually displays the original image, not only the thumbnail
# Version 0.17.0 - 2020-08-31 # Version 0.17.0 - 2020-08-31

View file

@ -0,0 +1,134 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:native_imaging/native_imaging.dart' as native;
import '../../utils/matrix_file_extension.dart';
import '../../utils/room_send_file_extension.dart';
import '../../components/dialogs/simple_dialogs.dart';
import '../../l10n/l10n.dart';
class SendFileDialog extends StatefulWidget {
final Room room;
final MatrixFile file;
const SendFileDialog({this.room, this.file, Key key}) : super(key: key);
@override
_SendFileDialogState createState() => _SendFileDialogState();
}
class _SendFileDialogState extends State<SendFileDialog> {
bool origImage = false;
Future<void> _send() async {
var file = widget.file;
if (file is MatrixImageFile && !origImage) {
final imgFile = file as MatrixImageFile;
// resize to max 1600 x 1600
try {
await native.init();
var nativeImg = native.Image();
try {
await nativeImg.loadEncoded(imgFile.bytes);
imgFile.width = nativeImg.width();
imgFile.height = nativeImg.height();
} on UnsupportedError {
final dartCodec = await instantiateImageCodec(imgFile.bytes);
final dartFrame = await dartCodec.getNextFrame();
imgFile.width = dartFrame.image.width;
imgFile.height = dartFrame.image.height;
final rgbaData = await dartFrame.image.toByteData();
final rgba = Uint8List.view(
rgbaData.buffer, rgbaData.offsetInBytes, rgbaData.lengthInBytes);
dartFrame.image.dispose();
dartCodec.dispose();
nativeImg.loadRGBA(imgFile.width, imgFile.height, rgba);
}
const max = 1600;
if (imgFile.width > max || imgFile.height > max) {
var w = max, h = max;
if (imgFile.width > imgFile.height) {
h = max * imgFile.height ~/ imgFile.width;
} else {
w = max * imgFile.width ~/ imgFile.height;
}
final scaledImg = nativeImg.resample(w, h, native.Transform.lanczos);
nativeImg.free();
nativeImg = scaledImg;
}
final jpegBytes = await nativeImg.toJpeg(75);
file = MatrixImageFile(
bytes: jpegBytes,
name: 'scaled_' + imgFile.name.split('.').first + '.jpg');
nativeImg.free();
} catch (e) {
// couldn't resize
}
}
await widget.room.sendFileEventWithThumbnail(file);
}
@override
Widget build(BuildContext context) {
var sendStr = L10n.of(context).sendFile;
if (widget.file is MatrixImageFile) {
sendStr = L10n.of(context).sendImage;
} else if (widget.file is MatrixAudioFile) {
sendStr = L10n.of(context).sendAudio;
} else if (widget.file is MatrixVideoFile) {
sendStr = L10n.of(context).sendVideo;
}
Widget contentWidget;
if (widget.file is MatrixImageFile) {
contentWidget = Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
Flexible(
child: Image.memory(
widget.file.bytes,
fit: BoxFit.contain,
),
),
Text(widget.file.name),
Row(
children: <Widget>[
Checkbox(
value: origImage,
onChanged: (v) => setState(() => origImage = v),
),
InkWell(
onTap: () => setState(() => origImage = !origImage),
child: Text(L10n.of(context).sendOriginal +
' (${widget.file.sizeString})'),
),
],
)
]);
} else {
contentWidget = Text('${widget.file.name} (${widget.file.sizeString})');
}
return AlertDialog(
title: Text(sendStr),
content: contentWidget,
actions: <Widget>[
FlatButton(
child: Text(L10n.of(context).cancel),
onPressed: () {
// just close the dialog
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(L10n.of(context).send),
onPressed: () async {
await SimpleDialogs(context).tryRequestWithLoadingDialog(_send());
await Navigator.of(context).pop();
},
),
],
);
}
}

View file

@ -13,6 +13,7 @@ import '../theme_switcher.dart';
import '../avatar.dart'; import '../avatar.dart';
import '../dialogs/simple_dialogs.dart'; import '../dialogs/simple_dialogs.dart';
import '../matrix.dart'; import '../matrix.dart';
import '../dialogs/send_file_dialog.dart';
class ChatListItem extends StatelessWidget { class ChatListItem extends StatelessWidget {
final Room room; final Room room;
@ -73,11 +74,12 @@ class ChatListItem extends StatelessWidget {
if (Matrix.of(context).shareContent != null) { if (Matrix.of(context).shareContent != null) {
if (Matrix.of(context).shareContent['msgtype'] == if (Matrix.of(context).shareContent['msgtype'] ==
'chat.fluffy.shared_file') { 'chat.fluffy.shared_file') {
await SimpleDialogs(context).tryRequestWithErrorToast( await showDialog(
room.sendFileEvent( context: context,
Matrix.of(context).shareContent['file'], builder: (context) => SendFileDialog(
), file: Matrix.of(context).shareContent['file'],
); room: room,
));
} else { } else {
unawaited(room.sendEvent(Matrix.of(context).shareContent)); unawaited(room.sendEvent(Matrix.of(context).shareContent));
} }

View file

@ -1,5 +1,5 @@
{ {
"@@last_modified": "2020-08-16T12:43:17.825046", "@@last_modified": "2020-09-04T14:58:35.809079",
"About": "About", "About": "About",
"@About": { "@About": {
"type": "text", "type": "text",
@ -1184,6 +1184,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Send audio": "Send audio",
"@Send audio": {
"type": "text",
"placeholders": {}
},
"Send file": "Send file", "Send file": "Send file",
"@Send file": { "@Send file": {
"type": "text", "type": "text",
@ -1194,6 +1199,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Send original": "Send original",
"@Send original": {
"type": "text",
"placeholders": {}
},
"Send video": "Send video",
"@Send video": {
"type": "text",
"placeholders": {}
},
"sentAFile": "{username} sent a file", "sentAFile": "{username} sent a file",
"@sentAFile": { "@sentAFile": {
"type": "text", "type": "text",

View file

@ -735,10 +735,16 @@ class L10n extends MatrixLocalizations {
String get sendAMessage => Intl.message("Send a message"); String get sendAMessage => Intl.message("Send a message");
String get sendAudio => Intl.message('Send audio');
String get sendFile => Intl.message('Send file'); String get sendFile => Intl.message('Send file');
String get sendImage => Intl.message('Send image'); String get sendImage => Intl.message('Send image');
String get sendOriginal => Intl.message('Send original');
String get sendVideo => Intl.message('Send video');
String sentAFile(String username) => Intl.message( String sentAFile(String username) => Intl.message(
"$username sent a file", "$username sent a file",
name: "sentAFile", name: "sentAFile",

View file

@ -394,8 +394,11 @@ class MessageLookup extends MessageLookupByLibrary {
"Send": MessageLookupByLibrary.simpleMessage("Send"), "Send": MessageLookupByLibrary.simpleMessage("Send"),
"Send a message": "Send a message":
MessageLookupByLibrary.simpleMessage("Send a message"), MessageLookupByLibrary.simpleMessage("Send a message"),
"Send audio": MessageLookupByLibrary.simpleMessage("Send audio"),
"Send file": MessageLookupByLibrary.simpleMessage("Send file"), "Send file": MessageLookupByLibrary.simpleMessage("Send file"),
"Send image": MessageLookupByLibrary.simpleMessage("Send image"), "Send image": MessageLookupByLibrary.simpleMessage("Send image"),
"Send original": MessageLookupByLibrary.simpleMessage("Send original"),
"Send video": MessageLookupByLibrary.simpleMessage("Send video"),
"Set a profile picture": "Set a profile picture":
MessageLookupByLibrary.simpleMessage("Set a profile picture"), MessageLookupByLibrary.simpleMessage("Set a profile picture"),
"Set group description": "Set group description":

View file

@ -31,4 +31,34 @@ extension MatrixFileExtension on MatrixFile {
} }
return; return;
} }
MatrixFile get detectFileType {
if (msgType == MessageTypes.Image) {
return MatrixImageFile(bytes: bytes, name: name);
}
if (msgType == MessageTypes.Video) {
return MatrixVideoFile(bytes: bytes, name: name);
}
if (msgType == MessageTypes.Audio) {
return MatrixAudioFile(bytes: bytes, name: name);
}
return this;
}
String get sizeString {
var size = this.size.toDouble();
if (size < 1000000) {
size = size / 1000;
size = (size * 10).round() / 10;
return '${size.toString()} KB';
} else if (size < 1000000000) {
size = size / 1000000;
size = (size * 10).round() / 10;
return '${size.toString()} MB';
} else {
size = size / 1000000000;
size = (size * 10).round() / 10;
return '${size.toString()} GB';
}
}
} }

View file

@ -23,11 +23,13 @@ import 'package:flutter/services.dart';
import 'package:memoryfilepicker/memoryfilepicker.dart'; import 'package:memoryfilepicker/memoryfilepicker.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:file_picker_platform_interface/file_picker_platform_interface.dart';
import 'chat_details.dart'; import 'chat_details.dart';
import 'chat_list.dart'; import 'chat_list.dart';
import '../components/input_bar.dart'; import '../components/input_bar.dart';
import '../utils/room_send_file_extension.dart'; import '../components/dialogs/send_file_dialog.dart';
import '../utils/matrix_file_extension.dart';
class ChatView extends StatelessWidget { class ChatView extends StatelessWidget {
final String id; final String id;
@ -191,39 +193,36 @@ class _ChatState extends State<_Chat> {
void sendFileAction(BuildContext context) async { void sendFileAction(BuildContext context) async {
var file = await MemoryFilePicker.getFile(); var file = await MemoryFilePicker.getFile();
if (file == null) return; if (file == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog( await showDialog(
room.sendFileEventWithThumbnail( context: context,
MatrixFile(bytes: file.bytes, name: file.path), builder: (context) => SendFileDialog(
), file:
); MatrixFile(bytes: file.bytes, name: file.path).detectFileType,
room: room,
));
} }
void sendImageAction(BuildContext context) async { void sendImageAction(BuildContext context) async {
var file = await MemoryFilePicker.getImage( var file = await MemoryFilePicker.getFile(type: FileType.image);
source: ImageSource.gallery,
imageQuality: 50,
maxWidth: 1600,
maxHeight: 1600);
if (file == null) return; if (file == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog( final bytes = await file.bytes;
room.sendFileEventWithThumbnail( await showDialog(
MatrixImageFile(bytes: await file.bytes, name: file.path), context: context,
), builder: (context) => SendFileDialog(
); file: MatrixImageFile(bytes: bytes, name: file.path),
room: room,
));
} }
void openCameraAction(BuildContext context) async { void openCameraAction(BuildContext context) async {
var file = await MemoryFilePicker.getImage( var file = await MemoryFilePicker.getImage(source: ImageSource.camera);
source: ImageSource.camera,
imageQuality: 50,
maxWidth: 1600,
maxHeight: 1600);
if (file == null) return; if (file == null) return;
await SimpleDialogs(context).tryRequestWithLoadingDialog( await showDialog(
room.sendFileEventWithThumbnail( context: context,
MatrixImageFile(bytes: file.bytes, name: file.path), builder: (context) => SendFileDialog(
), file: MatrixImageFile(bytes: file.bytes, name: file.path),
); room: room,
));
} }
void voiceMessageAction(BuildContext context) async { void voiceMessageAction(BuildContext context) async {
@ -235,12 +234,13 @@ class _ChatState extends State<_Chat> {
)); ));
if (result == null) return; if (result == null) return;
final audioFile = File(result); final audioFile = File(result);
await SimpleDialogs(context).tryRequestWithLoadingDialog( await showDialog(
room.sendFileEvent( context: context,
MatrixAudioFile( builder: (context) => SendFileDialog(
file: MatrixAudioFile(
bytes: audioFile.readAsBytesSync(), name: audioFile.path), bytes: audioFile.readAsBytesSync(), name: audioFile.path),
), room: room,
); ));
} }
String _getSelectedEventString(BuildContext context) { String _getSelectedEventString(BuildContext context) {

View file

@ -17,6 +17,7 @@ import '../components/matrix.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../utils/app_route.dart'; import '../utils/app_route.dart';
import '../utils/url_launcher.dart'; import '../utils/url_launcher.dart';
import '../utils/matrix_file_extension.dart';
import 'archive.dart'; import 'archive.dart';
import 'homeserver_picker.dart'; import 'homeserver_picker.dart';
import 'new_group.dart'; import 'new_group.dart';
@ -119,7 +120,7 @@ class _ChatListState extends State<ChatList> {
}); });
setState(() => null); setState(() => null);
}); });
_initReceiveSharingINtent(); _initReceiveSharingIntent();
super.initState(); super.initState();
} }
@ -139,7 +140,7 @@ class _ChatListState extends State<ChatList> {
'file': MatrixFile( 'file': MatrixFile(
bytes: file.readAsBytesSync(), bytes: file.readAsBytesSync(),
name: file.path, name: file.path,
), ).detectFileType,
}; };
} }
@ -158,7 +159,7 @@ class _ChatListState extends State<ChatList> {
}; };
} }
void _initReceiveSharingINtent() { void _initReceiveSharingIntent() {
if (kIsWeb) return; if (kIsWeb) return;
// For sharing images coming from outside the app while the app is in the memory // For sharing images coming from outside the app while the app is in the memory

View file

@ -248,6 +248,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.5.0" version: "0.5.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.1"
flutter_keyboard_visibility: flutter_keyboard_visibility:
dependency: transitive dependency: transitive
description: description:
@ -486,7 +493,7 @@ packages:
name: memoryfilepicker name: memoryfilepicker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.1" version: "0.1.3"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -1000,4 +1007,4 @@ packages:
version: "0.1.2" version: "0.1.2"
sdks: sdks:
dart: ">=2.10.0-0.0.dev <2.10.0" dart: ">=2.10.0-0.0.dev <2.10.0"
flutter: ">=1.18.0-6.0.pre <2.0.0" flutter: ">=1.20.0 <2.0.0"

View file

@ -31,7 +31,7 @@ dependencies:
localstorage: ^3.0.1+4 localstorage: ^3.0.1+4
bubble: ^1.1.9+1 bubble: ^1.1.9+1
memoryfilepicker: ^0.1.1 memoryfilepicker: ^0.1.3
url_launcher: ^5.4.1 url_launcher: ^5.4.1
url_launcher_web: ^0.1.0 url_launcher_web: ^0.1.0
flutter_advanced_networkimage: flutter_advanced_networkimage: