Add message entry frield visuals
This commit is contained in:
parent
4f67acec8e
commit
7b776db632
7 changed files with 231 additions and 17 deletions
|
@ -1,8 +1,12 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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 'config.dart';
|
||||
|
@ -21,6 +25,7 @@ class ApiClient {
|
|||
|
||||
ApiClient._internal();
|
||||
|
||||
final NeosHub _hub = NeosHub();
|
||||
AuthenticationData? _authenticationData;
|
||||
|
||||
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 {
|
||||
static final client = ApiClient();
|
||||
}
|
43
lib/aux.dart
Normal file
43
lib/aux.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,9 @@
|
|||
class Config {
|
||||
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";
|
||||
}
|
|
@ -14,8 +14,10 @@ class Message {
|
|||
final String senderId;
|
||||
final MessageType type;
|
||||
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) {
|
||||
final typeString = map["messageType"] as String?;
|
||||
|
@ -31,6 +33,7 @@ class Message {
|
|||
senderId: map["senderId"],
|
||||
type: type,
|
||||
content: map["content"],
|
||||
sendTime: DateTime.parse(map["sendTime"]),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import 'package:contacts_plus/apis/message_api.dart';
|
|||
import 'package:contacts_plus/models/friend.dart';
|
||||
import 'package:contacts_plus/models/message.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class Messages extends StatefulWidget {
|
||||
const Messages({required this.friend, super.key});
|
||||
|
@ -16,6 +17,9 @@ class Messages extends StatefulWidget {
|
|||
|
||||
class _MessagesState extends State<Messages> {
|
||||
Future<Iterable<Message>>? _messagesFuture;
|
||||
final TextEditingController _messageTextController = TextEditingController();
|
||||
|
||||
bool _isSendable = false;
|
||||
|
||||
void _refreshMessages() {
|
||||
_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}"),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
|
||||
setState(() {
|
||||
_refreshMessages();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
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 {
|
||||
const MyMessageBubble({required this.message, super.key});
|
||||
MyMessageBubble({required this.message, super.key});
|
||||
|
||||
final Message message;
|
||||
final DateFormat _dateFormat = DateFormat.Hm();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -98,12 +150,22 @@ class MyMessageBubble extends StatelessWidget {
|
|||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
margin: const EdgeInsets.only(left: 32, bottom: 16, right: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
content,
|
||||
softWrap: true,
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
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 {
|
||||
const OtherMessageBubble({required this.message, super.key});
|
||||
OtherMessageBubble({required this.message, super.key});
|
||||
|
||||
final Message message;
|
||||
final DateFormat _dateFormat = DateFormat.Hm();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -144,12 +207,22 @@ class OtherMessageBubble extends StatelessWidget {
|
|||
.secondaryContainer,
|
||||
margin: const EdgeInsets.only(right: 32, bottom: 16, left: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
content,
|
||||
softWrap: true,
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -158,3 +231,14 @@ class OtherMessageBubble extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageStatusIndicator extends StatelessWidget {
|
||||
const MessageStatusIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO: implement build
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
}
|
58
pubspec.lock
58
pubspec.lock
|
@ -152,6 +152,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -168,6 +176,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -184,6 +200,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -193,7 +217,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.8.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
||||
|
@ -208,6 +232,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -221,6 +253,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -261,6 +301,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: tuple
|
||||
sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -285,6 +333,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dart: ">=2.19.6 <3.0.0"
|
||||
flutter: ">=2.0.0"
|
||||
|
|
|
@ -38,6 +38,9 @@ dependencies:
|
|||
http: ^0.13.5
|
||||
uuid: ^3.0.7
|
||||
flutter_secure_storage: ^8.0.0
|
||||
intl: ^0.18.1
|
||||
path: ^1.8.2
|
||||
signalr_netcore: ^1.3.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue