Add basic image upload functionality
This commit is contained in:
parent
f411835c83
commit
362f0cef09
8 changed files with 167 additions and 69 deletions
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
@ -11,9 +10,10 @@ import 'package:contacts_plus_plus/models/records/asset_upload_data.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/preprocess_status.dart';
|
import 'package:contacts_plus_plus/models/records/preprocess_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/record.dart';
|
import 'package:contacts_plus_plus/models/records/record.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class AssetApi {
|
class RecordApi {
|
||||||
static Future<List<Record>> getRecordsAt(ApiClient client, {required String path}) async {
|
static Future<List<Record>> getRecordsAt(ApiClient client, {required String path}) async {
|
||||||
final response = await client.get("/users/${client.userId}/records?path=$path");
|
final response = await client.get("/users/${client.userId}/records?path=$path");
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponse(response);
|
||||||
|
@ -22,11 +22,12 @@ class AssetApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PreprocessStatus> preprocessRecord(ApiClient client, {required Record record}) async {
|
static Future<PreprocessStatus> preprocessRecord(ApiClient client, {required Record record}) async {
|
||||||
|
final body = jsonEncode(record.toMap());
|
||||||
final response = await client.post(
|
final response = await client.post(
|
||||||
"/users/${record.ownerId}/records/${record.id}/preprocess", body: jsonEncode(record.toMap()));
|
"/users/${record.ownerId}/records/${record.id}/preprocess", body: body);
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponse(response);
|
||||||
final body = jsonDecode(response.body);
|
final resultBody = jsonDecode(response.body);
|
||||||
return PreprocessStatus.fromMap(body);
|
return PreprocessStatus.fromMap(resultBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PreprocessStatus> getPreprocessStatus(ApiClient client,
|
static Future<PreprocessStatus> getPreprocessStatus(ApiClient client,
|
||||||
|
@ -40,7 +41,7 @@ class AssetApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
||||||
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks?bytes=${asset.bytes}");
|
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponse(response);
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
final res = AssetUploadData.fromMap(body);
|
final res = AssetUploadData.fromMap(body);
|
||||||
|
@ -54,14 +55,16 @@ class AssetApi {
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> uploadAsset(ApiClient client, {required NeosDBAsset asset, required Uint8List data}) async {
|
static Future<dynamic> uploadAsset(ApiClient client, {required String filename, required NeosDBAsset asset, required Uint8List data}) async {
|
||||||
final request = http.MultipartRequest(
|
final request = http.MultipartRequest(
|
||||||
"POST",
|
"POST",
|
||||||
ApiClient.buildFullUri("/users/${client.userId}/assets/${asset.hash}"),
|
ApiClient.buildFullUri("/users/${client.userId}/assets/${asset.hash}/chunks/0"),
|
||||||
)
|
)..files.add(http.MultipartFile.fromBytes("file", data, filename: filename, contentType: MediaType.parse("multipart/form-data")))
|
||||||
..files.add(http.MultipartFile.fromBytes("file", data));
|
..headers.addAll(client.authorizationHeader);
|
||||||
final response = await request.send();
|
final response = await request.send();
|
||||||
final body = jsonDecode(await response.stream.bytesToString());
|
final bodyBytes = await response.stream.toBytes();
|
||||||
|
ApiClient.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
|
||||||
|
final body = jsonDecode(bodyBytes.toString());
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,15 +76,16 @@ class AssetApi {
|
||||||
static Future<Record> uploadFile(ApiClient client, {required File file, required String machineId}) async {
|
static Future<Record> uploadFile(ApiClient client, {required File file, required String machineId}) async {
|
||||||
final data = await file.readAsBytes();
|
final data = await file.readAsBytes();
|
||||||
final asset = NeosDBAsset.fromData(data);
|
final asset = NeosDBAsset.fromData(data);
|
||||||
final assetUri = "neosdb://$machineId/${asset.hash}${extension(file.path)}";
|
final assetUri = "neosdb:///$machineId/${asset.hash}${extension(file.path)}";
|
||||||
final combinedRecordId = RecordId(id: Record.generateId(), ownerId: client.userId, isValid: true);
|
final combinedRecordId = RecordId(id: Record.generateId(), ownerId: client.userId, isValid: true);
|
||||||
|
final filename = basenameWithoutExtension(file.path);
|
||||||
final record = Record(
|
final record = Record(
|
||||||
id: 0,
|
id: combinedRecordId.id.toString(),
|
||||||
recordId: combinedRecordId.id.toString(),
|
|
||||||
combinedRecordId: combinedRecordId,
|
combinedRecordId: combinedRecordId,
|
||||||
assetUri: assetUri,
|
assetUri: assetUri,
|
||||||
name: basenameWithoutExtension(file.path),
|
name: filename,
|
||||||
tags: [
|
tags: [
|
||||||
|
filename,
|
||||||
"message_item",
|
"message_item",
|
||||||
"message_id:${Message.generateId()}"
|
"message_id:${Message.generateId()}"
|
||||||
],
|
],
|
||||||
|
@ -107,7 +111,7 @@ class AssetApi {
|
||||||
manifest: [
|
manifest: [
|
||||||
assetUri
|
assetUri
|
||||||
],
|
],
|
||||||
url: "neosrec://${client.userId}/${combinedRecordId.id}",
|
url: "neosrec:///${client.userId}/${combinedRecordId.id}",
|
||||||
isValidOwnerId: true,
|
isValidOwnerId: true,
|
||||||
isValidRecordId: true,
|
isValidRecordId: true,
|
||||||
visits: 0,
|
visits: 0,
|
||||||
|
@ -130,7 +134,7 @@ class AssetApi {
|
||||||
throw "Asset upload failed: ${uploadData.uploadState.name}";
|
throw "Asset upload failed: ${uploadData.uploadState.name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploadAsset(client, asset: asset, data: data);
|
await uploadAsset(client, asset: asset, data: data, filename: filename);
|
||||||
await finishUpload(client, asset: asset);
|
await finishUpload(client, asset: asset);
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ class UserApi {
|
||||||
final pkginfo = await PackageInfo.fromPlatform();
|
final pkginfo = await PackageInfo.fromPlatform();
|
||||||
status = status.copyWith(
|
status = status.copyWith(
|
||||||
neosVersion: "${pkginfo.version} of ${pkginfo.appName}",
|
neosVersion: "${pkginfo.version} of ${pkginfo.appName}",
|
||||||
|
isMobile: true,
|
||||||
);
|
);
|
||||||
final body = jsonEncode(status.toMap(shallow: true));
|
final body = jsonEncode(status.toMap(shallow: true));
|
||||||
final response = await client.put("/users/${client.userId}/status", body: body);
|
final response = await client.put("/users/${client.userId}/status", body: body);
|
||||||
|
|
|
@ -27,6 +27,7 @@ void main() async {
|
||||||
Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
|
Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
|
||||||
final settingsClient = SettingsClient();
|
final settingsClient = SettingsClient();
|
||||||
await settingsClient.loadSettings();
|
await settingsClient.loadSettings();
|
||||||
|
await settingsClient.changeSettings(settingsClient.currentSettings); // Save generated defaults to disk
|
||||||
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
|
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,8 @@ class RecordId {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Record {
|
class Record {
|
||||||
final int id;
|
final String id;
|
||||||
final RecordId combinedRecordId;
|
final RecordId combinedRecordId;
|
||||||
final String recordId;
|
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final String assetUri;
|
final String assetUri;
|
||||||
final int globalVersion;
|
final int globalVersion;
|
||||||
|
@ -73,7 +72,6 @@ class Record {
|
||||||
Record({
|
Record({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.combinedRecordId,
|
required this.combinedRecordId,
|
||||||
required this.recordId,
|
|
||||||
required this.isSynced,
|
required this.isSynced,
|
||||||
required this.fetchedOn,
|
required this.fetchedOn,
|
||||||
required this.path,
|
required this.path,
|
||||||
|
@ -105,9 +103,8 @@ class Record {
|
||||||
|
|
||||||
factory Record.fromMap(Map map) {
|
factory Record.fromMap(Map map) {
|
||||||
return Record(
|
return Record(
|
||||||
id: map["id"] ?? 0,
|
id: map["id"] ?? "0",
|
||||||
combinedRecordId: RecordId.fromMap(map["combinedRecordId"]),
|
combinedRecordId: RecordId.fromMap(map["combinedRecordId"]),
|
||||||
recordId: map["recordId"],
|
|
||||||
ownerId: map["ownerId"] ?? "",
|
ownerId: map["ownerId"] ?? "",
|
||||||
assetUri: map["assetUri"] ?? "",
|
assetUri: map["assetUri"] ?? "",
|
||||||
globalVersion: map["globalVersion"] ?? 0,
|
globalVersion: map["globalVersion"] ?? 0,
|
||||||
|
@ -139,7 +136,7 @@ class Record {
|
||||||
}
|
}
|
||||||
|
|
||||||
Record copyWith({
|
Record copyWith({
|
||||||
int? id,
|
String? id,
|
||||||
String? ownerId,
|
String? ownerId,
|
||||||
String? recordId,
|
String? recordId,
|
||||||
String? assetUri,
|
String? assetUri,
|
||||||
|
@ -175,7 +172,6 @@ class Record {
|
||||||
return Record(
|
return Record(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
ownerId: ownerId ?? this.ownerId,
|
ownerId: ownerId ?? this.ownerId,
|
||||||
recordId: recordId ?? this.recordId,
|
|
||||||
assetUri: assetUri ?? this.assetUri,
|
assetUri: assetUri ?? this.assetUri,
|
||||||
globalVersion: globalVersion ?? this.globalVersion,
|
globalVersion: globalVersion ?? this.globalVersion,
|
||||||
localVersion: localVersion ?? this.localVersion,
|
localVersion: localVersion ?? this.localVersion,
|
||||||
|
@ -210,12 +206,11 @@ class Record {
|
||||||
return {
|
return {
|
||||||
"id": id,
|
"id": id,
|
||||||
"ownerId": ownerId,
|
"ownerId": ownerId,
|
||||||
"recordId": recordId,
|
|
||||||
"assetUri": assetUri,
|
"assetUri": assetUri,
|
||||||
"globalVersion": globalVersion,
|
"globalVersion": globalVersion,
|
||||||
"localVersion": localVersion,
|
"localVersion": localVersion,
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": description,
|
"description": description.asNullable,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
"recordType": recordType.name,
|
"recordType": recordType.name,
|
||||||
"thumbnailUri": thumbnailUri,
|
"thumbnailUri": thumbnailUri,
|
||||||
|
@ -230,7 +225,7 @@ class Record {
|
||||||
"combinedRecordId": combinedRecordId.toMap(),
|
"combinedRecordId": combinedRecordId.toMap(),
|
||||||
"isSynced": isSynced,
|
"isSynced": isSynced,
|
||||||
"fetchedOn": fetchedOn.toUtc().toIso8601String(),
|
"fetchedOn": fetchedOn.toUtc().toIso8601String(),
|
||||||
"path": path,
|
"path": path.asNullable,
|
||||||
"manifest": manifest,
|
"manifest": manifest,
|
||||||
"url": url,
|
"url": url,
|
||||||
"isValidOwnerId": isValidOwnerId,
|
"isValidOwnerId": isValidOwnerId,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/models/friend.dart';
|
import 'package:contacts_plus_plus/models/friend.dart';
|
||||||
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class SettingsEntry<T> {
|
class SettingsEntry<T> {
|
||||||
final T? value;
|
final T? value;
|
||||||
|
@ -36,22 +37,25 @@ class Settings {
|
||||||
final SettingsEntry<bool> notificationsDenied;
|
final SettingsEntry<bool> notificationsDenied;
|
||||||
final SettingsEntry<int> lastOnlineStatus;
|
final SettingsEntry<int> lastOnlineStatus;
|
||||||
final SettingsEntry<String> lastDismissedVersion;
|
final SettingsEntry<String> lastDismissedVersion;
|
||||||
|
final SettingsEntry<String> machineId;
|
||||||
|
|
||||||
Settings({
|
Settings({
|
||||||
SettingsEntry<bool>? notificationsDenied,
|
SettingsEntry<bool>? notificationsDenied,
|
||||||
SettingsEntry<int>? lastOnlineStatus,
|
SettingsEntry<int>? lastOnlineStatus,
|
||||||
SettingsEntry<String>? lastDismissedVersion
|
SettingsEntry<String>? lastDismissedVersion,
|
||||||
|
SettingsEntry<String>? machineId
|
||||||
})
|
})
|
||||||
: notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
: notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
|
||||||
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
|
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
|
||||||
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString())
|
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString()),
|
||||||
;
|
machineId = machineId ?? SettingsEntry<String>(deflt: const Uuid().v4());
|
||||||
|
|
||||||
factory Settings.fromMap(Map map) {
|
factory Settings.fromMap(Map map) {
|
||||||
return Settings(
|
return Settings(
|
||||||
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
|
||||||
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
|
||||||
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"])
|
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"]),
|
||||||
|
machineId: retrieveEntryOrNull<String>(map["machineId"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +73,7 @@ class Settings {
|
||||||
"notificationsDenied": notificationsDenied.toMap(),
|
"notificationsDenied": notificationsDenied.toMap(),
|
||||||
"lastOnlineStatus": lastOnlineStatus.toMap(),
|
"lastOnlineStatus": lastOnlineStatus.toMap(),
|
||||||
"lastDismissedVersion": lastDismissedVersion.toMap(),
|
"lastDismissedVersion": lastDismissedVersion.toMap(),
|
||||||
|
"machineId": machineId.toMap(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,14 +81,15 @@ class Settings {
|
||||||
|
|
||||||
Settings copyWith({
|
Settings copyWith({
|
||||||
bool? notificationsDenied,
|
bool? notificationsDenied,
|
||||||
int? unreadCheckIntervalMinutes,
|
|
||||||
int? lastOnlineStatus,
|
int? lastOnlineStatus,
|
||||||
String? lastDismissedVersion,
|
String? lastDismissedVersion,
|
||||||
|
String? machineId,
|
||||||
}) {
|
}) {
|
||||||
return Settings(
|
return Settings(
|
||||||
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
||||||
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
|
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
|
||||||
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
|
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
|
||||||
|
machineId: this.machineId.passThrough(machineId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,19 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:contacts_plus_plus/apis/record_api.dart';
|
||||||
|
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/friend.dart';
|
import 'package:contacts_plus_plus/models/friend.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:contacts_plus_plus/models/message.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart';
|
import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'message_bubble.dart';
|
import 'message_bubble.dart';
|
||||||
|
@ -24,9 +32,11 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
final ScrollController _sessionListScrollController = ScrollController();
|
final ScrollController _sessionListScrollController = ScrollController();
|
||||||
final ScrollController _messageScrollController = ScrollController();
|
final ScrollController _messageScrollController = ScrollController();
|
||||||
|
|
||||||
bool _isSendable = false;
|
bool _hasText = false;
|
||||||
|
bool _isSending = false;
|
||||||
bool _showSessionListScrollChevron = false;
|
bool _showSessionListScrollChevron = false;
|
||||||
bool _showBottomBarShadow = false;
|
bool _showBottomBarShadow = false;
|
||||||
|
File? _loadedFile;
|
||||||
|
|
||||||
double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0;
|
double get _shevronOpacity => _showSessionListScrollChevron ? 1.0 : 0.0;
|
||||||
|
|
||||||
|
@ -69,6 +79,77 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> sendTextMessage(ScaffoldMessengerState scaffoldMessenger, ApiClient client, MessagingClient mClient, String content) async {
|
||||||
|
setState(() {
|
||||||
|
_isSending = true;
|
||||||
|
});
|
||||||
|
final message = Message(
|
||||||
|
id: Message.generateId(),
|
||||||
|
recipientId: widget.friend.id,
|
||||||
|
senderId: client.userId,
|
||||||
|
type: MessageType.text,
|
||||||
|
content: content,
|
||||||
|
sendTime: DateTime.now().toUtc(),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
mClient.sendMessage(message);
|
||||||
|
_messageTextController.clear();
|
||||||
|
setState(() {});
|
||||||
|
} catch (e) {
|
||||||
|
scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Failed to send message\n$e",
|
||||||
|
maxLines: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isSending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendImageMessage(ScaffoldMessengerState scaffoldMessenger, ApiClient client, MessagingClient mClient, File file, machineId) async {
|
||||||
|
setState(() {
|
||||||
|
_isSending = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
var record = await RecordApi.uploadFile(
|
||||||
|
client,
|
||||||
|
file: file,
|
||||||
|
machineId: machineId,
|
||||||
|
);
|
||||||
|
final newUri = Aux.neosDbToHttp(record.assetUri);
|
||||||
|
record = record.copyWith(
|
||||||
|
assetUri: newUri,
|
||||||
|
thumbnailUri: newUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
final message = Message(
|
||||||
|
id: Message.generateId(),
|
||||||
|
recipientId: widget.friend.id,
|
||||||
|
senderId: client.userId,
|
||||||
|
type: MessageType.object,
|
||||||
|
content: jsonEncode(record.toMap()),
|
||||||
|
sendTime: DateTime.now().toUtc(),
|
||||||
|
);
|
||||||
|
mClient.sendMessage(message);
|
||||||
|
_messageTextController.clear();
|
||||||
|
_loadedFile = null;
|
||||||
|
} catch (e) {
|
||||||
|
scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Failed to send file\n$e",
|
||||||
|
maxLines: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isSending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final apiClient = ClientHolder
|
final apiClient = ClientHolder
|
||||||
|
@ -207,6 +288,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_isSending && _loadedFile != null) const LinearProgressIndicator(),
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
|
@ -227,30 +309,43 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
/*IconButton(
|
||||||
|
onPressed: _hasText ? null : _loadedFile == null ? () async {
|
||||||
|
//final machineId = ClientHolder.of(context).settingsClient.currentSettings.machineId.valueOrDefault;
|
||||||
|
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
||||||
|
|
||||||
|
if (result != null && result.files.single.path != null) {
|
||||||
|
setState(() {
|
||||||
|
_loadedFile = File(result.files.single.path!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} : () => setState(() => _loadedFile = null),
|
||||||
|
icon: _loadedFile == null ? const Icon(Icons.attach_file) : const Icon(Icons.close),
|
||||||
|
),*/
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
enabled: cache != null && cache.error == null,
|
enabled: cache != null && cache.error == null && _loadedFile == null,
|
||||||
autocorrect: true,
|
autocorrect: true,
|
||||||
controller: _messageTextController,
|
controller: _messageTextController,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
onChanged: (text) {
|
onChanged: (text) {
|
||||||
if (text.isNotEmpty && !_isSendable) {
|
if (text.isNotEmpty && !_hasText) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSendable = true;
|
_hasText = true;
|
||||||
});
|
});
|
||||||
} else if (text.isEmpty && _isSendable) {
|
} else if (text.isEmpty && _hasText) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSendable = false;
|
_hasText = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
hintText: "Message ${widget.friend
|
hintText: _loadedFile == null ? "Message ${widget.friend
|
||||||
.username}...",
|
.username}..." : "Send ${basename(_loadedFile?.path ?? "")}",
|
||||||
hintMaxLines: 1,
|
hintMaxLines: 1,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
|
@ -264,35 +359,13 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
padding: const EdgeInsets.only(left: 8, right: 4.0),
|
padding: const EdgeInsets.only(left: 8, right: 4.0),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
onPressed: _isSendable ? () async {
|
onPressed: _isSending ? null : () async {
|
||||||
setState(() {
|
if (_loadedFile == null) {
|
||||||
_isSendable = false;
|
await sendTextMessage(ScaffoldMessenger.of(context), apiClient, mClient, _messageTextController.text);
|
||||||
});
|
} else {
|
||||||
final message = Message(
|
await sendImageMessage(ScaffoldMessenger.of(context), apiClient, mClient, _loadedFile!, ClientHolder.of(context).settingsClient.currentSettings.machineId.valueOrDefault);
|
||||||
id: Message.generateId(),
|
|
||||||
recipientId: widget.friend.id,
|
|
||||||
senderId: apiClient.userId,
|
|
||||||
type: MessageType.text,
|
|
||||||
content: _messageTextController.text,
|
|
||||||
sendTime: DateTime.now().toUtc(),
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
mClient.sendMessage(message);
|
|
||||||
_messageTextController.clear();
|
|
||||||
setState(() {});
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text("Failed to send message\n$e",
|
|
||||||
maxLines: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_isSendable = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} : null,
|
},
|
||||||
iconSize: 28,
|
iconSize: 28,
|
||||||
icon: const Icon(Icons.send),
|
icon: const Icon(Icons.send),
|
||||||
),
|
),
|
||||||
|
|
18
pubspec.lock
18
pubspec.lock
|
@ -153,6 +153,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.4"
|
version: "6.1.4"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -214,6 +222,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.14"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -305,7 +321,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.6"
|
version: "0.13.6"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
|
|
|
@ -36,6 +36,7 @@ dependencies:
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
|
http_parser: ^4.0.2
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
flutter_secure_storage: ^8.0.0
|
flutter_secure_storage: ^8.0.0
|
||||||
intl: ^0.18.1
|
intl: ^0.18.1
|
||||||
|
@ -58,6 +59,7 @@ dependencies:
|
||||||
dynamic_color: ^1.6.5
|
dynamic_color: ^1.6.5
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
|
file_picker: ^5.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue