Add message entry frield visuals

This commit is contained in:
Nutcake 2023-04-30 09:01:59 +02:00 committed by Nils Rother
parent 4f67acec8e
commit 7b776db632
7 changed files with 231 additions and 17 deletions

View file

@ -1,8 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:contacts_plus/models/authentication_data.dart'; import 'package:contacts_plus/models/authentication_data.dart';
import 'package:signalr_netcore/http_connection_options.dart';
import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/hub_connection_builder.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'config.dart'; import 'config.dart';
@ -21,6 +25,7 @@ class ApiClient {
ApiClient._internal(); ApiClient._internal();
final NeosHub _hub = NeosHub();
AuthenticationData? _authenticationData; AuthenticationData? _authenticationData;
set authenticationData(value) => _authenticationData = value; set authenticationData(value) => _authenticationData = value;
@ -130,6 +135,20 @@ class ApiClient {
} }
} }
class NeosHub {
final HubConnection hubConnection;
late final Future<void>? _hubConnectedFuture;
NeosHub() : hubConnection = HubConnectionBuilder()
.withUrl(Config.neosHubUrl, options: HttpConnectionOptions())
.withAutomaticReconnect()
.build() {
_hubConnectedFuture = hubConnection.start()?.whenComplete(() {
log("Hub connection established");
});
}
}
class BaseClient { class BaseClient {
static final client = ApiClient(); static final client = ApiClient();
} }

43
lib/aux.dart Normal file
View file

@ -0,0 +1,43 @@
import 'package:contacts_plus/config.dart';
import 'package:path/path.dart' as p;
enum NeosDBEndpoint
{
def,
blob,
cdn,
videoCDN,
}
extension NeosStringExtensions on Uri {
static String dbSignature(Uri neosdb) => neosdb.pathSegments.length < 2 ? "" : p.basenameWithoutExtension(neosdb.pathSegments[1]);
static String? neosDBQuery(Uri neosdb) => neosdb.query.trim().isEmpty ? null : neosdb.query.substring(1);
static bool isLegacyNeosDB(Uri uri) => !(uri.scheme != "neosdb") && uri.pathSegments.length >= 2 && p.basenameWithoutExtension(uri.pathSegments[1]).length < 30;
Uri neosDBToHTTP(NeosDBEndpoint endpoint) {
var signature = dbSignature(this);
var query = neosDBQuery(this);
if (query != null) {
signature = "$signature/$query";
}
if (isLegacyNeosDB(this)) {
return Uri.parse(Config.legacyCloudUrl + signature);
}
String base;
switch (endpoint) {
case NeosDBEndpoint.blob:
base = Config.blobStorageUrl;
break;
case NeosDBEndpoint.cdn:
base = Config.neosCdnUrl;
break;
case NeosDBEndpoint.videoCDN:
base = Config.videoStorageUrl;
break;
case NeosDBEndpoint.def:
base = Config.neosAssetsUrl;
}
return Uri.parse(base + signature);
}
}

View file

@ -1,3 +1,9 @@
class Config { class Config {
static const String apiBaseUrl = "https://cloudx.azurewebsites.net"; static const String apiBaseUrl = "https://cloudx.azurewebsites.net";
static const String legacyCloudUrl = "https://neoscloud.blob.core.windows.net/assets/";
static const String blobStorageUrl = "https://cloudxstorage.blob.core.windows.net/assets/";
static const String videoStorageUrl = "https://cloudx-video.azureedge.net/";
static const String neosCdnUrl = "https://cloudx.azureedge.net/assets/";
static const String neosAssetsUrl = "https://cloudxstorage.blob.core.windows.net/assets/";
static const String neosHubUrl = "$apiBaseUrl/hub";
} }

View file

@ -14,8 +14,10 @@ class Message {
final String senderId; final String senderId;
final MessageType type; final MessageType type;
final String content; final String content;
final DateTime sendTime;
Message({required this.id, required this.recipientId, required this.senderId, required this.type, required this.content}); Message({required this.id, required this.recipientId, required this.senderId, required this.type,
required this.content, required this.sendTime});
factory Message.fromMap(Map map) { factory Message.fromMap(Map map) {
final typeString = map["messageType"] as String?; final typeString = map["messageType"] as String?;
@ -31,6 +33,7 @@ class Message {
senderId: map["senderId"], senderId: map["senderId"],
type: type, type: type,
content: map["content"], content: map["content"],
sendTime: DateTime.parse(map["sendTime"]),
); );
} }
} }

View file

@ -3,6 +3,7 @@ import 'package:contacts_plus/apis/message_api.dart';
import 'package:contacts_plus/models/friend.dart'; import 'package:contacts_plus/models/friend.dart';
import 'package:contacts_plus/models/message.dart'; import 'package:contacts_plus/models/message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class Messages extends StatefulWidget { class Messages extends StatefulWidget {
const Messages({required this.friend, super.key}); const Messages({required this.friend, super.key});
@ -16,6 +17,9 @@ class Messages extends StatefulWidget {
class _MessagesState extends State<Messages> { class _MessagesState extends State<Messages> {
Future<Iterable<Message>>? _messagesFuture; Future<Iterable<Message>>? _messagesFuture;
final TextEditingController _messageTextController = TextEditingController();
bool _isSendable = false;
void _refreshMessages() { void _refreshMessages() {
_messagesFuture = MessageApi.getUserMessages(userId: widget.friend.id)..then((value) => value.toList()); _messagesFuture = MessageApi.getUserMessages(userId: widget.friend.id)..then((value) => value.toList());
@ -55,7 +59,9 @@ class _MessagesState extends State<Messages> {
Text("Failed to load messages:\n${snapshot.error}"), Text("Failed to load messages:\n${snapshot.error}"),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
setState(() {
_refreshMessages();
});
}, },
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: const Text("Retry"), label: const Text("Retry"),
@ -67,14 +73,60 @@ class _MessagesState extends State<Messages> {
} }
}, },
), ),
bottomNavigationBar: BottomAppBar(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
child: TextField(
controller: _messageTextController,
maxLines: 4,
minLines: 1,
onChanged: (text) {
if (text.isNotEmpty && !_isSendable) {
setState(() {
_isSendable = true;
});
} else if (text.isEmpty && _isSendable) {
setState(() {
_isSendable = false;
});
}
},
decoration: InputDecoration(
isDense: true,
hintText: "Send a message to ${widget.friend.username}...",
hintStyle: Theme.of(context).textTheme.labelLarge?.copyWith(color: Colors.white54),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
splashRadius: 24,
onPressed: _isSendable ? () {} : null,
iconSize: 28,
icon: const Icon(Icons.send),
),
)
],
),
),
); );
} }
} }
class MyMessageBubble extends StatelessWidget { class MyMessageBubble extends StatelessWidget {
const MyMessageBubble({required this.message, super.key}); MyMessageBubble({required this.message, super.key});
final Message message; final Message message;
final DateFormat _dateFormat = DateFormat.Hm();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -98,12 +150,22 @@ class MyMessageBubble extends StatelessWidget {
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
margin: const EdgeInsets.only(left: 32, bottom: 16, right: 8), margin: const EdgeInsets.only(left: 32, bottom: 16, right: 8),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text( child: Column(
content, crossAxisAlignment: CrossAxisAlignment.end,
softWrap: true, children: [
maxLines: null, Text(
style: Theme.of(context).textTheme.bodyLarge, content,
softWrap: true,
maxLines: null,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 6,),
Text(
_dateFormat.format(message.sendTime),
style: Theme.of(context).textTheme.labelMedium?.copyWith(color: Colors.white54),
),
],
), ),
), ),
), ),
@ -115,9 +177,10 @@ class MyMessageBubble extends StatelessWidget {
class OtherMessageBubble extends StatelessWidget { class OtherMessageBubble extends StatelessWidget {
const OtherMessageBubble({required this.message, super.key}); OtherMessageBubble({required this.message, super.key});
final Message message; final Message message;
final DateFormat _dateFormat = DateFormat.Hm();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -144,12 +207,22 @@ class OtherMessageBubble extends StatelessWidget {
.secondaryContainer, .secondaryContainer,
margin: const EdgeInsets.only(right: 32, bottom: 16, left: 8), margin: const EdgeInsets.only(right: 32, bottom: 16, left: 8),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text( child: Column(
content, crossAxisAlignment: CrossAxisAlignment.start,
softWrap: true, children: [
maxLines: null, Text(
style: Theme.of(context).textTheme.bodyLarge, content,
softWrap: true,
maxLines: null,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 6,),
Text(
_dateFormat.format(message.sendTime),
style: Theme.of(context).textTheme.labelMedium?.copyWith(color: Colors.white54),
),
],
), ),
), ),
), ),
@ -157,4 +230,15 @@ class OtherMessageBubble extends StatelessWidget {
], ],
); );
} }
}
class MessageStatusIndicator extends StatelessWidget {
const MessageStatusIndicator({super.key});
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}
} }

View file

@ -152,6 +152,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -168,6 +176,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
logging:
dependency: transitive
description:
name: logging
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -184,6 +200,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
message_pack_dart:
dependency: transitive
description:
name: message_pack_dart
sha256: "71b9f0ff60e5896e60b337960bb535380d7dba3297b457ac763ccae807385b59"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -193,7 +217,7 @@ packages:
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
@ -208,6 +232,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
signalr_netcore:
dependency: "direct main"
description:
name: signalr_netcore
sha256: bfc6e4cb95e3c2c1d9691e8c582a72e2b3fee4cd380abb060eaf65e3c5c43b29
url: "https://pub.dev"
source: hosted
version: "1.3.3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -221,6 +253,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
sse_client:
dependency: transitive
description:
name: sse_client
sha256: "71bd826430b41ab20a69d85bf2dfe9f11cfe222938e681ada1aea71fc8adf348"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -261,6 +301,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.16" version: "0.4.16"
tuple:
dependency: transitive
description:
name: tuple
sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -285,6 +333,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sdks: sdks:
dart: ">=2.19.6 <3.0.0" dart: ">=2.19.6 <3.0.0"
flutter: ">=2.0.0" flutter: ">=2.0.0"

View file

@ -38,6 +38,9 @@ dependencies:
http: ^0.13.5 http: ^0.13.5
uuid: ^3.0.7 uuid: ^3.0.7
flutter_secure_storage: ^8.0.0 flutter_secure_storage: ^8.0.0
intl: ^0.18.1
path: ^1.8.2
signalr_netcore: ^1.3.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: