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: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
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 {
|
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";
|
||||||
}
|
}
|
|
@ -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"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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"
|
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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue