Skip to content

bruig: improve send and attach file UX #694

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions brclient/appstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,16 +535,24 @@ func (as *appState) prettyArgs(args *mdembeds.EmbeddedArgs) string {
// Embedded file.
if args.Download.IsEmpty() {
switch {
case len(args.Data) == 0:
case len(args.Data) == 0 && args.LocalFilename == "":
s += "[Empty link and data]"
case args.Typ == "":
s += "[Embedded untyped data]"
default:
// When args.Data == 0 and args.LocalFilename != "",
// it means the embedded was saved to the local db.
name := "Embedded file"
if args.Name != "" {
name = args.Name
name = strescape.PathElement(args.Name)
} else if args.LocalFilename != "" {
name = filepath.Base(args.LocalFilename)
}
s += fmt.Sprintf("[%s (%s - %q)]", name, hbytes(int64(len(args.Data))), args.Typ)
size := int64(len(args.Data))
if args.LocalFilename != "" {
size, _ = fileSize(as.c.FullEmbedPath(args.LocalFilename))
}
s += fmt.Sprintf("[%s (%s - %q)]", name, hbytes(size), args.Typ)
}

return s
Expand Down Expand Up @@ -2454,17 +2462,23 @@ func (as *appState) viewRaw(b []byte) (tea.Cmd, error) {
}

func (as *appState) viewEmbed(embedded mdembeds.EmbeddedArgs) (tea.Cmd, error) {
if len(embedded.Data) == 0 {
return nil, fmt.Errorf("no embedded file")
}
prog := programByMimeType(*as.mimeMap.Load(), embedded.Typ)
if prog == "" {
return nil, fmt.Errorf("no external viewer configured for %v", embedded.Typ)
}

filePath, err := as.c.SaveEmbed(embedded.Data, embedded.Typ)
if err != nil {
return nil, err
var filePath string
switch {
case embedded.LocalFilename != "":
filePath = as.c.FullEmbedPath(embedded.LocalFilename)
case len(embedded.Data) != 0:
var err error
filePath, err = as.c.SaveEmbed(embedded.Data, embedded.Typ)
if err != nil {
return nil, err
}
default:
return nil, errors.New("embedded is empty")
}

c := exec.Command(prog, filePath)
Expand Down
1 change: 1 addition & 0 deletions brclient/embedwidget.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (ew *embedWidget) tryEmbed() error {
id = chainhash.HashH(data).String()[:8]
pseudoData := fmt.Sprintf("[content %s]", id)
args.Data = []byte(pseudoData)
args.Name = filepath.Base(filename)
}

if ew.idxSharedFile > -1 && ew.idxSharedFile < len(ew.sharedFiles) {
Expand Down
9 changes: 9 additions & 0 deletions brclient/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,12 @@ func suggestedBrclientVersion(clients []rpc.SuggestedClientVersion) (newVersion
needsUpdate = version.IsOtherVersionHigher(newVersion)
return
}

// fileSize reads the size of a file.
func fileSize(filepath string) (int64, error) {
stat, err := os.Stat(filepath)
if err != nil {
return 0, err
}
return stat.Size(), nil
}
176 changes: 100 additions & 76 deletions bruig/flutterui/bruig/lib/components/attach_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import 'package:bruig/components/md_elements.dart';
import 'package:bruig/components/chat/types.dart';
import 'package:bruig/components/empty_widget.dart';
import 'package:bruig/components/text.dart';
import 'package:bruig/models/client.dart';
import 'package:bruig/models/snackbar.dart';
import 'package:bruig/screens/compress.dart';
import 'package:bruig/screens/send_file.dart';
import 'package:bruig/theme_manager.dart';
import 'package:bruig/util.dart';
import 'package:flutter_avif/flutter_avif.dart';
Expand Down Expand Up @@ -43,15 +45,24 @@ class AttachmentEmbed {
String? alt;
String id;
String? filename;
String? name;
AttachmentEmbed(this.id,
{this.data, this.linkedFile, this.alt, this.mime, this.filename});
{this.data,
this.linkedFile,
this.alt,
this.mime,
this.filename,
this.name});

String displayString() {
return "--embed[id=$id]--";
}

String embedString() {
List<String> parts = [];
if ((name ?? "") != "") {
parts.add("name=${Uri.encodeComponent(name!)}");
}
if ((alt ?? "") != "") {
parts.add("alt=${Uri.encodeComponent(alt!)}");
}
Expand Down Expand Up @@ -84,11 +95,21 @@ class AttachmentEmbed {
}
}

class _GalleryImg {
final File file;
final ImageProvider img;

_GalleryImg(this.file, this.img);
}

class AttachFileScreen extends StatefulWidget {
final SendMsg _send;
final Uint8List? initialFileData;
final String? initialMime;
final ChatModel chat;
final VoidCallback closeAttachScreen;
const AttachFileScreen(this._send, this.initialFileData, this.initialMime,
this.chat, this.closeAttachScreen,
{super.key});

@override
Expand All @@ -104,7 +125,7 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
SharedFileAndShares? linkedFile;
Timer? _debounce;
late Future<Directory?> _futureGetPath;
List<dynamic> listImagePath = [];
List<_GalleryImg> listImages = [];
Directory _fetchedPath = Directory.systemTemp;
bool _permissionStatus = false;

Expand Down Expand Up @@ -150,17 +171,24 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
return await getDownloadsDirectory();
}

final RegExp imgExtRegexp =
RegExp(".(gif|jpe?g|tiff?|png|webp|bmp)", caseSensitive: false);

_fetchFiles(Directory dir) {
List<dynamic> listImage = [];
List<_GalleryImg> newList = [];
dir.list().forEach((element) {
RegExp regExp =
RegExp(".(gif|jpe?g|tiff?|png|webp|bmp)", caseSensitive: false);
// Only add in List if path is an image
if (regExp.hasMatch('$element')) listImage.add(element);
setState(() {
listImagePath = listImage;
});
if (!imgExtRegexp.hasMatch(element.path)) return;
if (element is! File) return;
try {
var imgProvider = Image.file(element).image;
newList.add(_GalleryImg(element, imgProvider));
} catch (_) {
// Ignore invalid image.
}
});

listImages = newList;

WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {
_fetchedPath = dir;
}));
Expand All @@ -180,28 +208,9 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
if (filePath == null) return;
filePath = filePath.trim();
if (filePath == "") return;
var data = await File(filePath).readAsBytes();

if (data.length > Golib.maxPayloadSize) {
throw "File is too large to attach (limit: ${Golib.maxPayloadSizeStr})";
}

var mimeType = lookupMimeType(filePath);
if (mimeType == null) {
throw "Unable to lookup file type";
}
if (!allowedMimeTypes.contains(mimeType)) {
throw "Selected file ($filePath) type not allowed, only $allowedMimeTypes currently allowed";
}
setState(() {
selectedAttachmentPath = filePath;
this.filePath = filePath!;
fileName = path.basename(filePath);
fileData = data;
mime = mimeType;
});
} on Exception catch (exception) {
snackbar.error("Unable to attach file: $exception");
await showSendFileScreen(context,
chat: widget.chat, file: File(filePath));
widget.closeAttachScreen(); // File screen already does the sending.
} catch (exception) {
snackbar.error("Unable to attach file: $exception");
}
Expand All @@ -219,52 +228,68 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
required BuildContext context,
}) async {
var snackbar = SnackBarModel.of(context);
XFile? pickedFile;
try {
final XFile? pickedFile = await _picker.pickImage(
source: source,
);
if (pickedFile == null) {
return;
}

var data = await pickedFile.readAsBytes();
if (data.length > Golib.maxPayloadSize) {
throw "File is too large to attach (limit: ${Golib.maxPayloadSizeStr})";
}
var mimeType = lookupMimeType(pickedFile.path);
setState(() {
selectedAttachmentPath = pickedFile.path;
fileData = data;
if (mimeType != null) {
mime = mimeType;
}
});
pickedFile = await _picker.pickImage(source: source);
} catch (e) {
snackbar.error("Unable to attach file: $e");
snackbar.error("Unable to select file: $e");
}

if (pickedFile == null) {
return;
}

_onImagePressed(pickedFile.path, context: context);
}

Future<void> _onImagePressed(
File image, {
String filePath, {
required BuildContext context,
}) async {
var snackbar = SnackBarModel.of(context);
try {
var data = await image.readAsBytes();
var mimeType = lookupMimeType(filePath);
if (mimeType == null) {
throw "Unknown image MIME type";
}
if (!mimeType.startsWith("image/")) {
throw "Not an image mime type ($mimeType)";
}

var data = await File(filePath).readAsBytes();
if (data.length > Golib.maxPayloadSize) {
throw "File is too large to attach (limit: ${Golib.maxPayloadSizeStr})";
// Image is larger than possible to attach as message. Automatically show
// compression screen to try and reduce the max size.
var compressRes =
await showCompressScreen(context, original: data, mime: mimeType);

if (compressRes == null) {
// User canceled compression.
return;
}

if (compressRes.data.length > Golib.maxPayloadSize) {
// Compression was insufficient to reduce size. This needs to be sent
// as a file.
await showSendFileScreen(context,
chat: widget.chat, file: File(filePath));
return;
}

// Replace the vars so that the setState below uses the compressed version.
data = compressRes.data;
mimeType = compressRes.mime;
}
var mimeType = lookupMimeType(image.path);

setState(() {
selectedAttachmentPath = image.path;
selectedAttachmentPath = filePath;
fileData = data;
fileName = path.basename(image.path);
if (mimeType != null) {
mime = mimeType;
}
});
} catch (e) {
snackbar.error("Unable to select image: $e");
snackbar.error("Unable to attach file: $e");
}
}

Expand All @@ -281,6 +306,9 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
alt: alt,
mime: mime,
filename: fileName,
name: selectedAttachmentPath != null
? path.basename(selectedAttachmentPath!)
: null,
);
widget._send(embed.embedString());

Expand Down Expand Up @@ -376,26 +404,25 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
if (_permissionStatus && _fetchedPath != dir) {
_fetchFiles(dir);
}
return listImagePath.isNotEmpty
? Text(dir.path)
: const Empty();
return listImages.isNotEmpty ? Text(dir.path) : const Empty();
} else {
return const Text("Loading gallery");
}
},
),
listImagePath.isNotEmpty
listImages.isNotEmpty
? Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
height: 150.0,
child: ListView(
child: Scrollbar(
controller: scrollCtrl,
scrollDirection: Axis.horizontal,
primary: false,
padding: const EdgeInsets.all(10),
children: [
for (var i = 0; i < listImagePath.length; i++)
Padding(
child: ListView.builder(
controller: scrollCtrl,
scrollDirection: Axis.horizontal,
primary: false,
padding: const EdgeInsets.all(10),
itemCount: listImages.length,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.all(10),
child: Material(
shape: const RoundedRectangleBorder(
Expand All @@ -405,7 +432,7 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
borderRadius: const BorderRadius.all(
Radius.circular(10)),
onTap: () => _onImagePressed(
listImagePath[i],
listImages[i].file.path,
context: context),
child: Container(
width: 100,
Expand All @@ -416,13 +443,10 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
const BorderRadius.all(
Radius.circular(8.0)),
image: DecorationImage(
image:
Image.file(listImagePath[i])
.image,
image: listImages[i].img,
fit: BoxFit.contain),
),
))))
]))
)))))))
: const Empty(),
Row(children: [
const SizedBox(width: 10),
Expand Down
4 changes: 2 additions & 2 deletions bruig/flutterui/bruig/lib/components/chat/chat_side_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ class _ChatSideMenuState extends State<ChatSideMenu> {
itemCount: menus.length,
itemBuilder: (context, index) => ListTile(
title: Txt.S(menus[index].label),
onTap: () {
menus[index].onSelected(context, client);
onTap: () async {
await menus[index].onSelected(context, client);
client.ui.chatSideMenuActive.clear();
}))),
]),
Expand Down
Loading
Loading