chat attachment added.

design_3.0_cx_module
Sikander Saleem 2 weeks ago
parent c5b4325aec
commit 7445902564

@ -4,13 +4,13 @@ class URLs {
static const String appReleaseBuildNumber = "28";
// static const host1 = "https://atomsm.hmg.com"; // production url
static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
// static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static const host1 = "http://10.201.111.125:9495"; // temporary Server UAT url
static String _baseUrl = "$_host/mobile";
// static String _baseUrl = "$_host/mobile";
// static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
// static final String _baseUrl = "$_host/v4/mobile"; // for asset inventory on UAT
// static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:google_api_availability/google_api_availability.dart';
// import 'package:mohem_flutter_app/app_state/app_state.dart';
// import 'package:mohem_flutter_app/classes/colors.dart';
// import 'package:mohem_flutter_app/config/routes.dart';
@ -23,6 +24,7 @@ import 'package:nfc_manager/platform_tags.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/dialogs/loading_dialog.dart';
// ignore_for_file: avoid_annotating_with_dynamic
@ -49,19 +51,21 @@ class Utils {
return null;
}
}
static String getOrdinal(int number) {
if (number >= 11 && number <= 13) return "${number}th";
switch (number % 10) {
case 1:
return "${number}st";
case 2:
return "${number}nd";
case 3:
return "${number}rd";
default:
return "${number}th";
static String getOrdinal(int number) {
if (number >= 11 && number <= 13) return "${number}th";
switch (number % 10) {
case 1:
return "${number}st";
case 2:
return "${number}nd";
case 3:
return "${number}rd";
default:
return "${number}th";
}
}
}
static int stringToHex(String colorCode) {
try {
return int.parse(colorCode.replaceAll("#", "0xff"));
@ -73,7 +77,8 @@ static String getOrdinal(int number) {
static Future delay(int millis) async {
return await Future.delayed(Duration(milliseconds: millis));
}
static bool isBeforeOrEqualCurrentTime(TimeOfDay t1, TimeOfDay t2) {
static bool isBeforeOrEqualCurrentTime(TimeOfDay t1, TimeOfDay t2) {
return t1.hour < t2.hour || (t1.hour == t2.hour && t1.minute <= t2.minute);
}
@ -102,6 +107,21 @@ static String getOrdinal(int number) {
_isLoadingVisible = false;
}
static void showUploadingDialog(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_isLoadingVisible = true;
showDialog(
context: context,
barrierColor: Colors.black.withOpacity(0.5),
useRootNavigator: false,
builder: (BuildContext context) => const UploadingDialog(),
).then((value) {
_isLoadingVisible = false;
});
});
}
static Future<String> getStringFromPrefs(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(key) ?? "";
@ -176,6 +196,7 @@ static String getOrdinal(int number) {
),
);
}
//
// static Widget getNoDataWidget(BuildContext context) {
// return Column(
@ -421,15 +442,15 @@ static String getOrdinal(int number) {
}
return false;
}
//
// static bool isDate(String input, String format) {
// try {
// DateTime d = DateFormat(format).parseStrict(input);
// //print(d);
// return true;
// } catch (e) {
// //print(e);
// return false;
// }
// }
//
// static bool isDate(String input, String format) {
// try {
// DateTime d = DateFormat(format).parseStrict(input);
// //print(d);
// return true;
// } catch (e) {
// //print(e);
// return false;
// }
// }
}

@ -55,8 +55,6 @@ class _AssetRetiredState extends State<AssetRetired> with TickerProviderStateMix
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: DefaultAppBar(title: context.translation.assetToBeRetired),
@ -125,9 +123,9 @@ class _AssetRetiredState extends State<AssetRetired> with TickerProviderStateMix
onPressed: () async {
requestDetailProvider.assetRetiredHelperModel?.activityAssetToBeRetiredAttachments = [];
for (var item in _attachments) {
String fileName = ServiceRequestUtils.isLocalUrl(item.name??'') ? ("${item.name??''.split("/").last}|${base64Encode(File(item.name??'').readAsBytesSync())}") :item.name??'';
requestDetailProvider.assetRetiredHelperModel?.activityAssetToBeRetiredAttachments
?.add(ActivityAssetToBeRetiredAttachments(id: item.id, name: fileName));
String fileName =
ServiceRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
requestDetailProvider.assetRetiredHelperModel?.activityAssetToBeRetiredAttachments?.add(ActivityAssetToBeRetiredAttachments(id: item.id, name: fileName));
}
int status = await requestDetailProvider.createActivityAssetToBeRetired();
if (status == 200) {

@ -176,7 +176,7 @@ class _ServiceRequestDetailMainState extends State<ServiceRequestDetailMain> {
void getChatToken(int moduleId, String title) {
ChatProvider cProvider = Provider.of<ChatProvider>(context, listen: false);
if (cProvider.chatLoginResponse != null) return;
if (cProvider.chatLoginResponse != null && cProvider.referenceID == widget.requestId) return;
String assigneeEmployeeNumber = Provider.of<ServiceRequestDetailProvider>(context, listen: false).currentWorkOrder?.data?.assignedEmployee?.employeeId ?? "";
String myEmployeeId = context.userProvider.user!.username!;

@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/extensions/string_extensions.dart';
@ -75,6 +76,16 @@ class ChatApiClient {
}
}
Future<ChatParticipantModel?> viewAllDocuments(int moduleId, int referenceId) async {
Response response = await ApiClient().getJsonForResponse("${URLs.chatHubUrlApi}/attachments/conversation?referenceId=$referenceId&moduleCode=$moduleId", token: chatLoginResponse!.token);
if (response.statusCode == 200) {
return ChatParticipantModel.fromJson(jsonDecode(response.body));
} else {
return null;
}
}
Future<List<SingleUserChatModel>> loadChatHistory(int moduleId, int referenceId, String myId, String otherId) async {
Response response = await ApiClient().postJsonForResponse(
"${URLs.chatHubUrlApi}/UserChatHistory/GetUserChatHistory/$myId/$otherId/0", {"moduleCode": moduleId.toString(), "referenceId": referenceId.toString()},
@ -203,26 +214,25 @@ class ChatApiClient {
// }
// Upload Chat Media
// Future<Object?> uploadMedia(String userId, File file, String fileSource) async {
// if (kDebugMode) {
// print("${ApiConsts.chatMediaImageUploadUrl}upload");
// print(AppState().chatDetails!.response!.token);
// }
//
// dynamic request = MultipartRequest('POST', Uri.parse('${ApiConsts.chatMediaImageUploadUrl}upload'));
// request.fields.addAll({'userId': userId, 'fileSource': fileSource});
// request.files.add(await MultipartFile.fromPath('files', file.path));
// request.headers.addAll({'Authorization': 'Bearer ${AppState().chatDetails!.response!.token}'});
// StreamedResponse response = await request.send();
// String data = await response.stream.bytesToString();
// if (!kReleaseMode) {
// logger.i("res: " + data);
// }
// return jsonDecode(data);
// }
Future<Object?> uploadMedia(String userId, File file, String fileSource, {Map? jsonData}) async {
dynamic request = MultipartRequest('POST', Uri.parse('${URLs.chatHubUrlApi}/attachments/upload'));
request.fields.addAll({'userId': userId, 'fileSource': fileSource});
if (jsonData != null) {
request.fields.addAll(jsonData);
}
// Download File For Chat
// Future<Uint8List> downloadURL({required String fileName, required String fileTypeDescription, required int fileSource}) async {
request.files.add(await MultipartFile.fromPath('file', file.path));
request.headers.addAll({'Authorization': 'Bearer ${chatLoginResponse!.token}'});
StreamedResponse response = await request.send();
String data = await response.stream.bytesToString();
if (!kReleaseMode) {
print("uploadMedia: $data");
}
return jsonDecode(data);
}
// // Download File For Chat
// Future<Uint8List> downloadURL({required String fileName, required String fileTypeDescription, required int fileSource,required String url}) async {
// Response response = await ApiClient().postJsonForResponse(
// "${ApiConsts.chatMediaImageUploadUrl}download",
// {"fileType": fileTypeDescription, "fileName": fileName, "fileSource": fileSource},
@ -232,6 +242,83 @@ class ChatApiClient {
// return data;
// }
// Download File For Chat
Future<Uint8List> downloadURL(String url, {required String fileName, required String fileTypeDescription, required int fileSource}) async {
Response response = await ApiClient().postJsonForResponse(
url,
{"fileType": fileTypeDescription, "fileName": fileName, "fileSource": fileSource},
token: chatLoginResponse!.token,
);
Uint8List data = Uint8List.fromList(response.bodyBytes);
return data;
}
/// Downloads a file from a secure URL that uses a 302 redirect.
///
/// [url]: The initial URL that requires the authorization token.
/// [token]: The Bearer token for authorization.
/// [saveFileName]: The name to give the downloaded file (e.g., 'my-document.pdf').
Future<File?> downloadFileWithHttp(String url, {required String fileName, required String fileTypeDescription, required int fileSource}) async {
final client = http.Client();
File? file;
try {
final request = http.Request('GET', Uri.parse(url));
request.headers['Authorization'] = 'Bearer ${chatLoginResponse!.token}';
// This is the most important part: prevent automatic redirection.
request.followRedirects = false;
print("Making initial request to: $url");
final streamedResponse = await client.send(request);
// --- Step 2: Check the response and handle the redirect ---
// Check for a 302 redirect status.
if (streamedResponse.statusCode == 302) {
// Get the new URL from the 'Location' header.
final redirectUrl = streamedResponse.headers['location'];
if (redirectUrl == null) {
throw Exception('302 Redirect did not contain a Location header.');
}
final fileResponse = await http.get(Uri.parse(redirectUrl), headers: {'Authorization': 'Bearer ${chatLoginResponse!.token}'});
if (fileResponse.statusCode == 200) {
// Get a safe directory to save the file.
final dir = await getTemporaryDirectory();
file = File('${dir.path}/$fileName');
// Write the file to disk.
await file.writeAsBytes(fileResponse.bodyBytes);
print("File downloaded successfully and saved to: ${file.path}");
} else {
// The download from the final URL failed.
throw Exception('Failed to download from redirect URL. Status: ${fileResponse.statusCode}');
}
}
// Handle cases where the server might just send the file directly.
else if (streamedResponse.statusCode == 200) {
print("Server sent file directly without redirect.");
final dir = await getTemporaryDirectory();
file = File('${dir.path}/$fileName');
await file.writeAsBytes(await streamedResponse.stream.toBytes());
print("File downloaded successfully and saved to: ${file.path}");
}
// Handle other error statuses.
else {
throw Exception('Failed to initiate download. Status: ${streamedResponse.statusCode}');
}
} catch (e) {
print("An error occurred during download: $e");
} finally {
// Always close the client to free up resources.
client.close();
}
return file;
}
// //Get Chat Users & Favorite Images
// Future<List<ChatUserImageModel>> getUsersImages({required List<String> encryptedEmails}) async {
// List<ChatUserImageModel> imagesData = [];

@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
@ -8,15 +11,21 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.dart';
import 'package:test_sa/models/service_request/service_request.dart';
import 'package:test_sa/modules/cm_module/service_request_detail_provider.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:test_sa/modules/cx_module/chat/view_all_attachment_page.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/sound/sound_player.dart';
import 'helper/chat_audio_player.dart';
import 'helper/chat_file_picker.dart';
import 'helper/chat_file_viewer.dart';
import 'model/get_single_user_chat_list_model.dart';
import 'model/user_chat_history_model.dart';
@ -74,6 +83,7 @@ class _ChatPageState extends State<ChatPage> {
@override
void dispose() {
chatHubConnection.stop();
playerController.dispose();
recorderController.dispose();
super.dispose();
@ -113,14 +123,37 @@ class _ChatPageState extends State<ChatPage> {
Container(
color: AppColor.neutral50,
constraints: const BoxConstraints(maxHeight: 56),
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8),
alignment: Alignment.center,
child: Row(
children: [
Text(
chatProvider.recipient?.userName ?? "",
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
chatProvider.recipient?.userName ?? "",
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10),
),
AnimatedSize(
duration: const Duration(milliseconds: 250),
child: SizedBox(
height: chatProvider.isTyping ? null : 0,
child: Text(
"Typing...",
maxLines: 1,
style: AppTextStyles.overline.copyWith(color: AppColor.white10),
),
)),
// if (chatProvider.isTyping)
// Text(
// "Typing...",
// maxLines: 1,
// style: AppTextStyles.tinyFont2.copyWith(color: AppColor.white10),
// ),
],
).expanded,
4.width,
Text(
@ -130,7 +163,9 @@ class _ChatPageState extends State<ChatPage> {
decoration: TextDecoration.underline,
decorationColor: AppColor.white10,
),
),
).onPress(() {
Navigator.push(context, CupertinoPageRoute(builder: (context) => ViewAllAttachmentPage(moduleId: 1, requestId: widget.requestId)));
}),
],
),
),
@ -196,8 +231,14 @@ class _ChatPageState extends State<ChatPage> {
maxLines: 3,
textInputAction: TextInputAction.none,
keyboardType: TextInputType.multiline,
onTap: () {
chatHubConnection.invoke("SendTypingAsync", args: [receiver]);
},
onTapOutside: (PointerDownEvent event) {
chatHubConnection.invoke("SendStopTypingAsync", args: [receiver]);
},
onChanged: (text) {
chatHubConnection.invoke("SendTypingAsync", args: [context.userProvider.user!.username!]);
chatHubConnection.invoke("SendTypingAsync", args: [receiver]);
},
decoration: InputDecoration(
enabledBorder: InputBorder.none,
@ -214,7 +255,23 @@ class _ChatPageState extends State<ChatPage> {
),
).expanded,
IconButton(
onPressed: () {},
onPressed: () async {
FocusScope.of(context).unfocus();
File? file = (await showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
builder: (BuildContext context) => ChatFilePicker())) as File?;
if (file != null) {
Utils.showUploadingDialog(context);
await chatProvider.uploadAttachments(context.userProvider.user!.username!, file, "1");
Utils.hideLoading(context);
}
},
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
),
@ -458,24 +515,41 @@ class _ChatPageState extends State<ChatPage> {
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onPressed: () {
chatProvider.invokeSendMessage({
"Contant": textEditingController.text,
// "ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f",
// "CreatedDate": "2025-11-09T18:58:12.502Z",
"CurrentEmployeeNumber": context.userProvider.user!.username!,
"ChatEventId": 1,
"ConversationId": chatProvider.chatParticipantModel!.id!.toString(),
"ModuleCode": widget.moduleId.toString(),
"ReferenceNumber": widget.requestId.toString(),
"UserChatHistoryLineRequestList": [
{"TargetEmployeeNumber": receiver, "TargetUserStatus": 1, "IsSeen": false, "IsDelivered": true, "SeenOn": null, "DeliveredOn": null}
]
}).then((success) {
if (success) {
textEditingController.clear();
onPressed: () async {
if (chatState == ChatState.voiceRecordingCompleted) {
Utils.showUploadingDialog(context);
try {
await chatProvider.uploadAttachments(context.userProvider.user!.username!, File(recordedFilePath!), "1");
Utils.hideLoading(context);
await playerController.stopPlayer();
recorderController.reset();
recordedFilePath = null;
chatState = ChatState.idle;
setState(() {});
} catch (ex) {
Utils.hideLoading(context);
}
});
} else {
if (textEditingController.text.isEmpty) return;
chatProvider.invokeSendMessage({
"Contant": textEditingController.text,
// "ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f",
// "CreatedDate": "2025-11-09T18:58:12.502Z",
"CurrentEmployeeNumber": context.userProvider.user!.username!,
"ChatEventId": 1,
"ConversationId": chatProvider.chatParticipantModel!.id!.toString(),
"ModuleCode": widget.moduleId.toString(),
"ReferenceNumber": widget.requestId.toString(),
"UserChatHistoryLineRequestList": [
{"TargetEmployeeNumber": receiver, "TargetUserStatus": 1, "IsSeen": false, "IsDelivered": true, "SeenOn": null, "DeliveredOn": null}
]
}).then((success) {
if (success) {
textEditingController.clear();
}
});
}
},
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
@ -555,10 +629,51 @@ class _ChatPageState extends State<ChatPage> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
chatResponse?.contant ?? msg,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.neutral120),
).toShimmer(context: context, isShow: loading),
if (chatResponse?.downloadUrl != null) ...[
// if (chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "audio")
// // ASoundPlayer(audio: chatResponse.downloadUrl!)
//
// ChatAudioPlayer(chatResponse!.fileTypeResponse!, chatResponse.downloadUrl!)
// // if (
// //
// // chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "image"
// //
// // )
//
// else
ChatFileViewer(chatResponse!.downloadUrl!, chatResponse.fileTypeResponse!),
// Container(
// width: 250,
// height: 250,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(12),
// border: Border.all(color: Colors.grey.withOpacity(.5), width: 1),
// image: DecorationImage(
// image: NetworkImage(chatResponse.downloadUrl!, headers: {'Authorization': "Bearer ${Provider.of<ChatProvider>(context, listen: false).chatLoginResponse!.token!}"}),
// ),
// ),
// // child: CachedNetworkImage(
// // imageUrl: chatResponse.downloadUrl! ?? "",
// // // fit: boxFit ?? BoxFit.cover,
// // // alignment: Alignment.center,
// // // width: width,
// // // height: height,
// // httpHeaders: {'Authorization': "Bearer ${Provider.of<ChatProvider>(context, listen: false).chatLoginResponse!.token!}"},
// // placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
// // // errorWidget: (context, url, error) => Icon(showDefaultIcon ? Icons.image_outlined : Icons.broken_image_rounded),
// // ),
//
// // Image.network(
// // chatResponse.downloadUrl!,
// // headers: {'Authorization': "Bearer ${Provider.of<ChatProvider>(context, listen: false).chatLoginResponse!.token!}"},
// // ),
// )
] else
Text(
chatResponse?.contant ?? msg,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.neutral120),
).toShimmer(context: context, isShow: loading),
if (loading) 4.height,
Text(
chatResponse?.createdDate?.toString().chatMsgTime ?? "2:00 PM",
@ -628,6 +743,9 @@ class _ChatPageState extends State<ChatPage> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (chatResponse?.downloadUrl != null) ...[
if (chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "audio") ChatAudioPlayer(chatResponse!.fileTypeResponse!, chatResponse.downloadUrl!)
],
Text(
contentMsg,
style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10),

@ -106,6 +106,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
//
// bool disbaleChatForThisUser = false;
bool isTyping = false;
bool chatLoginTokenLoading = false;
ChatLoginResponse? chatLoginResponse;
@ -125,6 +127,10 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
Participants? sender;
Participants? recipient;
late String receiverID;
late int moduleID;
int? referenceID;
void reset() {
chatLoginTokenLoading = false;
chatParticipantLoading = false;
@ -153,6 +159,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
Future<void> getUserAutoLoginTokenSilent(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber) async {
reset();
receiverID = assigneeEmployeeNumber;
chatLoginTokenLoading = true;
notifyListeners();
try {
@ -186,9 +193,17 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// } catch (e) {}
// }
Future<void> viewAllDocuments(int moduleId, int requestId) async {
try {
await ChatApiClient().viewAllDocuments(moduleId, requestId);
} catch (ex) {}
}
Future<void> connectToHub(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async {
userChatHistoryLoading = true;
notifyListeners();
moduleID = moduleId;
referenceID = requestId;
await buildHubConnection(chatParticipantModel!.id!.toString());
userChatHistory = await ChatApiClient().loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber);
chatResponseList = userChatHistory ?? [];
@ -239,22 +254,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// return returnStatus;
// }
void sendMsgSignalR() {
var abc = {
"Contant": "Follow-up: Test results look good.",
"ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f",
"CreatedDate": "2025-11-09T18:58:12.502Z",
"CurrentEmployeeNumber": "EMP123456",
"ChatEventId": 1,
"ConversationId": "15521",
"ModuleCode": "CRM",
"ReferenceNumber": "CASE-55231",
"UserChatHistoryLineRequestList": [
{"TargetEmployeeNumber": "EMP654321", "TargetUserStatus": 1, "IsSeen": false, "IsDelivered": true, "SeenOn": null, "DeliveredOn": null}
]
};
}
// List<groups.GroupResponse>? uGroups = [], searchGroups = [];
// Future<void> getUserAutoLoginToken() async {
@ -287,6 +286,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
chatHubConnection.on("OnMessageReceivedAsync", onMsgReceived);
chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync);
chatHubConnection.on("OnTypingAsync", OnTypingAsync);
chatHubConnection.on("OnStopTypingAsync", OnStopTypingAsync);
//group On message
@ -389,34 +389,28 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// chatCID = uuid.v4();
// }
// void markRead(List<SingleUserChatModel> data, int receiverID) {
// for (SingleUserChatModel element in data!) {
// if (AppState().chatDetails!.response!.id! == element.targetUserId) {
// if (element.isSeen != null) {
// if (!element.isSeen!) {
// element.isSeen = true;
// dynamic data = [
// {
// "userChatHistoryId": element.userChatHistoryId,
// "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId,
// "isDelivered": true,
// "isSeen": true,
// }
// ];
// updateUserChatHistoryStatusAsync(data);
// notifyListeners();
// }
// }
// for (ChatUser element in searchedChats!) {
// if (element.id == receiverID) {
// element.unreadMessageCount = 0;
// chatUConvCounter = 0;
// }
// }
// }
// }
// notifyListeners();
// }
void markRead(List<SingleUserChatModel> data, String receiverID) {
for (SingleUserChatModel element in data) {
// if (AppState().chatDetails!.response!.id! == element.targetUserId) {
if (element.isSeen != null) {
if (!element.isSeen!) {
element.isSeen = true;
dynamic data = [
{
"userChatHistoryId": element.userChatHistoryId,
"TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId,
"isDelivered": true,
"isSeen": true,
}
];
updateUserChatHistoryStatusAsync(data);
notifyListeners();
}
// }
}
}
// notifyListeners();
}
void updateUserChatHistoryStatusAsync(List data) {
try {
@ -434,25 +428,51 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
}
}
List<SingleUserChatModel> getSingleUserChatModel(String str) => List<SingleUserChatModel>.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x)));
// List<SingleUserChatModel> getSingleUserChatModel(String str) => List<SingleUserChatModel>.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x)));
List<SingleUserChatModel> getSingleUserChatModel(String str) {
final dynamic decodedJson = json.decode(str);
// Check if the decoded JSON is already a List
if (decodedJson is List) {
return List<SingleUserChatModel>.from(decodedJson.map((x) => SingleUserChatModel.fromJson(x)));
}
// If it's a Map (a single object), wrap it in a list
else if (decodedJson is Map<String, dynamic>) {
return [SingleUserChatModel.fromJson(decodedJson)];
}
// Handle unexpected types
else {
throw const FormatException('Expected a JSON object or a list of JSON objects.');
}
}
// List<groupchathistory.GetGroupChatHistoryAsync> getGroupChatHistoryAsync(String str) =>
// List<groupchathistory.GetGroupChatHistoryAsync>.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x)));
//
// Future<dynamic> uploadAttachments(String userId, File file, String fileSource) async {
// dynamic result;
// try {
// Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource);
// if (response != null) {
// result = response;
// } else {
// result = [];
// }
// } catch (e) {
// throw e;
// }
// return result;
// }
Future<dynamic> uploadAttachments(String userId, File file, String fileSource) async {
dynamic result;
try {
Map<String, String> jsonData = {
"IsContextual": true.toString(),
"ModuleCode": moduleID.toString(),
"ReferenceId": referenceID.toString(),
"ReferenceType": "ticket",
"ConversationId": chatParticipantModel!.id.toString(),
"TargetUserId": receiverID,
"SendMessage": true.toString(),
};
Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource, jsonData: jsonData);
if (response != null) {
result = response;
} else {
result = [];
}
} catch (e) {
throw e;
}
return result;
}
// void updateUserChatStatus(List<Object?>? args) {
// dynamic items = args!.toList();
@ -555,8 +575,27 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// notifyListeners();
// }
Timer? timer;
Future<void> OnTypingAsync(List<Object?>? parameters) async {
print("OnTypingAsync:$parameters");
// String empId = parameters!.first as String;
isTyping = true;
notifyListeners();
if (timer?.isActive ?? false) {
timer!.cancel();
}
timer = Timer(const Duration(milliseconds: 2500), () {
isTyping = false;
notifyListeners();
});
}
Future<void> OnStopTypingAsync(List<Object?>? parameters) async {
if (timer?.isActive ?? false) {
timer!.cancel();
}
isTyping = false;
notifyListeners();
}
// Future<void> OnSubmitChatAsync(List<Object?>? parameters) async {
@ -632,11 +671,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// );
// }
// }
setMsgTune();
// setMsgTune();
// userChatHistory = userChatHistory! + data;
// chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!));
userChatHistory?.insert(0, data.first);
notifyListeners();
// markRead(data, data.first.targetUserId!);
// if (isChatScreenActive && data.first.currentUserId == receiverID) {
//
// } else {
@ -760,6 +801,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// }
void OnSubmitChatAsync(List<Object?>? parameters) {
print("OnSubmitChatAsync:$parameters");
List<SingleUserChatModel> data = [];
for (dynamic msg in parameters!) {
data = getSingleUserChatModel(jsonEncode(msg));
@ -1215,7 +1257,7 @@ String getFileTypeDescription(String value) {
// }
// }
// }
//
// void sendChatMessage(
// BuildContext context, {
// required int targetUserId,

@ -0,0 +1,138 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cx_module/chat/api_client.dart';
import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart';
import 'package:test_sa/modules/cx_module/chat/model/get_single_user_chat_list_model.dart';
import '../../../../new_views/app_style/app_color.dart';
class ChatAudioPlayer extends StatefulWidget {
String downloadURl;
FileTypeResponse file;
bool isLocalFile;
ChatAudioPlayer(this.file, this.downloadURl, {Key? key, this.isLocalFile = false}) : super(key: key);
@override
_ChatAudioPlayerState createState() {
return _ChatAudioPlayerState();
}
}
class _ChatAudioPlayerState extends State<ChatAudioPlayer> {
bool downloading = false;
PlayerController playerController = PlayerController();
@override
void initState() {
super.initState();
checkFileInLocalStorage();
}
File? audioFile;
List<double> waveformData = [];
void checkFileInLocalStorage() async {
Directory tempDir = await getTemporaryDirectory();
String tempPath = '${tempDir.path}/${widget.file.fileName}';
File tempFile = File(tempPath);
bool exists = await tempFile.exists();
if (exists) {
audioFile = tempFile;
playerController = PlayerController();
await playerController.preparePlayer(path: tempPath);
waveformData = await playerController.extractWaveformData(path: tempPath);
} else {
downloadFile();
}
}
void downloadFile() async {
downloading = true;
setState(() {});
try {
// Uint8List list = await ChatApiClient().downloadFileWithHttp(widget.downloadURl, fileName: widget.file.fileName, fileTypeDescription: widget.file.fileTypeDescription, fileSource: widget.file.fileTypeId!);
// audioFile = File.fromRawPath(list);
audioFile =
await ChatApiClient().downloadFileWithHttp(widget.downloadURl, fileName: widget.file.fileName, fileTypeDescription: widget.file.fileTypeDescription, fileSource: widget.file.fileTypeId!);
playerController = PlayerController();
await playerController.preparePlayer(path: audioFile!.path);
waveformData = await playerController.extractWaveformData(path: audioFile!.path);
} catch (ex) {
print(ex);
audioFile = null;
}
downloading = false;
setState(() {});
}
@override
void dispose() {
playerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 56,
child: Row(
children: [
if (downloading)
const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2),
)
else if (playerController.playerState == PlayerState.playing)
IconButton(
onPressed: () async {
await playerController.pausePlayer();
await playerController.stopPlayer();
setState(() {});
},
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
),
icon: const Icon(Icons.stop_circle_outlined, size: 20),
constraints: const BoxConstraints(),
)
else
IconButton(
onPressed: () async {
await playerController.startPlayer();
setState(() {});
},
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded
),
icon: const Icon(Icons.play_circle_fill_rounded, size: 20),
constraints: const BoxConstraints(),
),
AudioFileWaveforms(
playerController: playerController,
waveformData: waveformData,
enableSeekGesture: false,
continuousWaveform: false,
waveformType: WaveformType.long,
playerWaveStyle: const PlayerWaveStyle(
fixedWaveColor: AppColor.neutral50,
liveWaveColor: AppColor.primary10,
showSeekLine: true,
),
size: Size(MediaQuery.of(context).size.width, 56.0),
).expanded,
],
),
);
}
}

@ -0,0 +1,436 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
class ChatFilePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Attach File".heading4(context),
12.height,
GridView(
padding: const EdgeInsets.all(0),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12),
children: <Widget>[
gridItem(Icons.camera_enhance_rounded, context.translation.pickFromCamera).onPress(() async {
await fromMediaPicker(context, ImageSource.camera);
}),
gridItem(Icons.image_rounded, context.translation.pickFromGallery).onPress(() async {
await fromMediaPicker(context, ImageSource.gallery);
}),
gridItem(Icons.file_present_rounded, context.translation.pickFromFiles).onPress(() async {
await fromFilePicker(context);
}),
],
),
12.height,
],
).paddingAll(21);
}
fromMediaPicker(BuildContext context, ImageSource imageSource) async {
XFile? pickedFile = await ImagePicker().pickImage(source: imageSource, imageQuality: 70, maxWidth: 800, maxHeight: 800);
if (pickedFile != null) {
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1),
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'ATOMS',
toolbarColor: Colors.white,
toolbarWidgetColor: context.settingProvider.theme == "dark" ? AppColor.neutral10 : AppColor.neutral50,
initAspectRatio: CropAspectRatioPreset.square,
lockAspectRatio: false,
),
IOSUiSettings(title: 'ATOMS'),
],
);
if (croppedFile != null) {
Navigator.pop(context, File(croppedFile.path));
return;
}
}
Navigator.pop(context);
}
fromFilePicker(BuildContext context) async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xlsx', 'pptx'],
);
if ((result?.paths ?? []).isNotEmpty) {
Navigator.pop(context, File(result!.paths.first!));
} else {
Navigator.pop(context);
}
}
Widget gridItem(IconData iconData, String title) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xffF1F1F1), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(iconData, color: const Color(0xff7D859A), size: 36),
Text(
title,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
],
),
);
}
// static Future<File?> selectFile(BuildContext context) async {
// ImageSource source = (await showModalBottomSheet(
// context: context,
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(
// top: Radius.circular(20),
// ),
// ),
// clipBehavior: Clip.antiAliasWithSaveLayer,
// builder: (BuildContext context) => ChatFilePicker())) as ImageSource;
//
// final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
// }
}
// class AttachmentPicker extends StatefulWidget {
// final String label;
// final bool error;
// final List<GenericAttachmentModel> attachment;
//
// final bool enabled, onlyImages;
// double? buttonHeight;
// Widget? buttonIcon;
// Color? buttonColor;
// final Function(List<GenericAttachmentModel>)? onChange;
// final bool showAsGrid;
//
// AttachmentPicker(
// {Key? key,
// this.attachment = const <GenericAttachmentModel>[],
// required this.label,
// this.error = false,
// this.buttonHeight,
// this.buttonIcon,
// this.enabled = true,
// this.onlyImages = false,
// this.onChange,
// this.showAsGrid = false,
// this.buttonColor})
// : super(key: key);
//
// @override
// State<AttachmentPicker> createState() => _AttachmentPickerState();
// }
//
// class _AttachmentPickerState extends State<AttachmentPicker> {
// @override
// Widget build(BuildContext context) {
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// AppDashedButton(
// title: widget.label,
// height: widget.buttonHeight,
// buttonColor: widget.buttonColor,
// icon: widget.buttonIcon,
// onPressed: (widget.enabled == false)
// ? () {}
// : widget.showAsGrid
// ? showFileSourceSheet
// : onFilePicker),
// 16.height,
// if (widget.attachment.isNotEmpty)
// Wrap(
// spacing: 8.toScreenWidth,
// children: List.generate(
// widget.attachment.length,
// (index) {
// File image = File(widget.attachment[index].name!);
// return MultiFilesPickerItem(
// file: image,
// enabled: widget.enabled,
// onRemoveTap: (image) {
// if (!widget.enabled) {
// return;
// }
// widget.attachment.removeAt(index);
// if (widget.onChange != null) {
// widget.onChange!(widget.attachment);
// }
// setState(() {});
// },
// );
// },
// ),
// ),
// ],
// );
// }
//
// fromFilePicker() async {
// FilePickerResult? result = await FilePicker.platform.pickFiles(
// type: FileType.custom,
// allowMultiple: true,
// allowedExtensions: widget.onlyImages ? ['jpg', 'jpeg', 'png'] : ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xlsx', 'pptx'],
// );
// if (result != null) {
// for (var path in result.paths) {
// widget.attachment.add(GenericAttachmentModel(id: 0, name: File(path!).path));
// }
// if (widget.onChange != null) {
// widget.onChange!(widget.attachment);
// }
// setState(() {});
// }
// }
//
// void showFileSourceSheet() async {
// // if (widget.attachment.length >= 5) {
// // Fluttertoast.showToast(msg: context.translation.maxImagesNumberIs5);
// // return;
// // }
//
// ImageSource source = (await showModalBottomSheet(
// context: context,
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(
// top: Radius.circular(20),
// ),
// ),
// clipBehavior: Clip.antiAliasWithSaveLayer,
// builder: (BuildContext context) => Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// "Attach File".heading4(context),
// 12.height,
// GridView(
// padding: const EdgeInsets.all(0),
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12),
// children: <Widget>[
// gridItem(Icons.camera_enhance_rounded, context.translation.pickFromCamera).onPress(() => Navigator.of(context).pop(ImageSource.camera)),
// gridItem(Icons.image_rounded, context.translation.pickFromGallery).onPress(() => Navigator.of(context).pop(ImageSource.gallery)),
// gridItem(Icons.file_present_rounded, context.translation.pickFromFiles).onPress(() async {
// await fromFilePicker();
// Navigator.pop(context);
// }),
// ],
// ),
// 12.height,
// ],
// ).paddingAll(21),
// )) as ImageSource;
//
// final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
//
// if (pickedFile != null) {
// File fileImage = File(pickedFile.path);
// widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path));
// if (widget.onChange != null) {
// widget.onChange!(widget.attachment);
// }
// setState(() {});
// }
// }
//
// Widget gridItem(IconData iconData, String title) {
// return Container(
// padding: const EdgeInsets.all(12),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// border: Border.all(color: const Color(0xffF1F1F1), width: 1),
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Icon(iconData, color: const Color(0xff7D859A), size: 36),
// Text(
// title,
// style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
// ),
// ],
// ),
// );
// }
//
// onFilePicker() async {
// //TODO removed on request by Backend as they don't have anyissue with large number of files
// // if (widget.attachment.length >= 5) {
// // Fluttertoast.showToast(msg: context.translation.maxImagesNumberIs5);
// // return;
// // }
// ImageSource? source = await showModalBottomSheet<ImageSource>(
// context: context,
// builder: (BuildContext context) {
// Widget listCard({required String icon, required String label, required VoidCallback onTap}) {
// return Container(
// padding: const EdgeInsets.all(12),
// decoration: BoxDecoration(
// color: AppColor.background(context),
// // color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// border: Border.all(color: const Color(0xffF1F1F1), width: 1),
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// icon.toSvgAsset(color: AppColor.iconColor(context), width: 36, height: 36),
// // Icon(iconData, color: const Color(0xff7D859A), size: 36),
// Text(
// label,
// style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
// ),
// ],
// ),
// ).onPress(onTap);
// }
//
// return SafeArea(
// top: false,
// child: Container(
// width: double.infinity,
// color: AppColor.background(context),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// "Attach File".heading4(context),
// 12.height,
// GridView(
// padding: const EdgeInsets.all(0),
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12),
// children: <Widget>[
// listCard(
// icon: 'camera_icon',
// label: '${context.translation.open}\n${context.translation.camera}',
// onTap: () {
// Navigator.of(context).pop(ImageSource.camera);
// },
// ),
// listCard(
// icon: 'gallery_icon',
// label: '${context.translation.open}\n${context.translation.gallery}',
// onTap: () {
// Navigator.of(context).pop(ImageSource.gallery);
// },
// ),
// listCard(
// icon: 'file_icon',
// label: '${context.translation.open}\n${context.translation.files}',
// onTap: () async {
// await fromFilePicker();
// Navigator.pop(context);
// },
// ),
// ],
// ),
// // Container(
// // padding: const EdgeInsets.all(16.0),
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
// // children: <Widget>[
// // listCard(
// // icon: 'camera_icon',
// // label: '${context.translation.open}\n${context.translation.camera}',
// // onTap: () {
// // Navigator.of(context).pop(ImageSource.camera);
// // },
// // ),
// // listCard(
// // icon: 'gallery_icon',
// // label: '${context.translation.open}\n${context.translation.gallery}',
// // onTap: () {
// // Navigator.of(context).pop(ImageSource.gallery);
// // },
// // ),
// // listCard(
// // icon: 'file_icon',
// // label: '${context.translation.open}\n${context.translation.files}',
// // onTap: () async {
// // await fromFilePicker();
// // Navigator.pop(context);
// // },
// // ),
// // ],
// // ),
// // ),
// ],
// ).paddingAll(16),
// ),
// );
// },
// );
// // ImageSource source = await showDialog(
// // context: context,
// // builder: (dialogContext) => CupertinoAlertDialog(
// // actions: <Widget>[
// // TextButton(
// // child: Text(context.translation.pickFromCamera),
// // onPressed: () {
// // Navigator.of(dialogContext).pop(ImageSource.camera);
// // },
// // ),
// // TextButton(
// // child: Text(context.translation.pickFromGallery),
// // onPressed: () {
// // Navigator.of(dialogContext).pop(ImageSource.gallery);
// // },
// // ),
// // TextButton(
// // child: Text(context.translation.pickFromFiles),
// // onPressed: () async {
// // await fromFilePicker();
// // Navigator.pop(context);
// // },
// // ),
// // ],
// // ),
// // );
// if (source == null) return;
//
// final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800);
//
// if (pickedFile != null) {
// File fileImage = File(pickedFile.path);
// widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path));
// if (widget.onChange != null) {
// widget.onChange!(widget.attachment);
// }
// setState(() {});
// }
//
// setState(() {});
// }
// }

@ -0,0 +1,137 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:test_sa/modules/cx_module/chat/model/get_single_user_chat_list_model.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/app_style/app_text_style.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker_item.dart';
import 'package:test_sa/views/widgets/sound/sound_player.dart';
class ChatFileViewer extends StatelessWidget {
String downloadUrl;
FileTypeResponse fileTypeResponse;
ChatFileViewer(this.downloadUrl, this.fileTypeResponse, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder<File?>(
future: checkFileInLocalStorage(),
builder: (BuildContext context, AsyncSnapshot<File?> snapshot) {
if (snapshot.connectionState != ConnectionState.waiting && snapshot.hasData) {}
if (fileTypeResponse!.fileKind.toString().toLowerCase() == "audio") {
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
width: 48,
height: 48,
decoration: ShapeDecoration(
color: AppColor.background(context),
shape: RoundedRectangleBorder(
side: BorderSide(width: 1, color: (context.isDark ? AppColor.neutral20 : AppColor.neutral30)),
borderRadius: BorderRadius.circular(32),
),
),
child: const CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2).center,
);
}
if (snapshot.data == null) {
return Text("Failed to load", style: AppTextStyle.tiny.copyWith(color: context.isDark ? AppColor.red50 : AppColor.red60));
}
return ASoundPlayer(audio: snapshot.data!.path);
}
return Container(
width: 180,
height: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.withOpacity(.5), width: 1),
image: snapshot.hasData ? DecorationImage(fit: BoxFit.contain, image: getImage(snapshot.data!)) : null,
),
child: snapshot.connectionState == ConnectionState.waiting
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2),
).center
: snapshot.hasData
? getFile(context, snapshot.data!)
: const Icon(Icons.broken_image_rounded),
);
});
}
Widget getFile(BuildContext context, File file) {
bool isImage = file.path.split(".").last.toLowerCase() == "png" || file.path.split(".").last.toLowerCase() == "jpg" || file.path.split(".").last.toLowerCase() == "jpeg";
bool isPdf = file.path.split(".").last.toLowerCase() == "pdf";
bool isExcel = file.path.split(".").last.toLowerCase() == "xlsx";
return MaterialButton(
padding: EdgeInsets.zero,
onPressed: () async {
if (isImage) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: const DefaultAppBar(),
body: SafeArea(
child: InteractiveViewer(child: Image.file(file)).center,
),
),
),
);
} else {
OpenFile.open(file.path);
}
// else {
// // if (!await launchUrl(Uri.parse(URLs.getFileUrl(file.path)!), mode: LaunchMode.externalApplication)) {
// // Fluttertoast.showToast(msg: "UnExpected Error with file.");
// // throw Exception('Could not launch');
// // }
// }
},
);
}
ImageProvider getImage(File file) {
bool isImage = file.path.split(".").last.toLowerCase() == "png" || file.path.split(".").last.toLowerCase() == "jpg" || file.path.split(".").last.toLowerCase() == "jpeg";
bool isPdf = file.path.split(".").last.toLowerCase() == "pdf";
bool isExcel = file.path.split(".").last.toLowerCase() == "xlsx";
if (isImage) {
return FileImage(file);
}
return AssetImage("assets/images/${isPdf ? "pdf" : isExcel ? "excel" : "doc"}.png");
}
Future<File?> checkFileInLocalStorage() async {
Directory tempDir = await getTemporaryDirectory();
String tempPath = '${tempDir.path}/${fileTypeResponse.fileName}';
File tempFile = File(tempPath);
bool exists = await tempFile.exists();
if (exists) {
return tempFile;
} else {
return downloadFile();
}
}
Future<File?> downloadFile() async {
try {
return await ChatApiClient()
.downloadFileWithHttp(downloadUrl, fileName: fileTypeResponse.fileName, fileTypeDescription: fileTypeResponse.fileTypeDescription, fileSource: fileTypeResponse.fileTypeId!);
} catch (ex) {
print("downloadFile:$ex");
return null;
}
}
}

@ -29,6 +29,7 @@ class SingleUserChatModel {
this.createdDate,
this.chatSource,
this.conversationId,
this.downloadUrl,
this.fileTypeResponse,
this.userChatReplyResponse,
this.isReplied,
@ -56,6 +57,7 @@ class SingleUserChatModel {
DateTime? createdDate;
int? chatSource;
String? conversationId;
String? downloadUrl;
FileTypeResponse? fileTypeResponse;
UserChatReplyResponse? userChatReplyResponse;
bool? isReplied;
@ -84,6 +86,7 @@ class SingleUserChatModel {
createdDate: json["createdDate"] == null ? null : DateTime.parse(json["createdDate"]),
chatSource: json["chatSource"] == null ? null : json["chatSource"],
conversationId: json["conversationId"] == null ? null : json["conversationId"],
downloadUrl: json["downloadUrl"] == null ? null : json["downloadUrl"],
fileTypeResponse: json["fileTypeResponse"] == null ? null : FileTypeResponse.fromJson(json["fileTypeResponse"]),
userChatReplyResponse: json["userChatReplyResponse"] == null ? null : UserChatReplyResponse.fromJson(json["userChatReplyResponse"]),
isReplied: false,
@ -112,6 +115,7 @@ class SingleUserChatModel {
"createdDate": createdDate == null ? null : createdDate!.toIso8601String(),
"chatSource": chatSource == null ? null : chatSource,
"conversationId": conversationId == null ? null : conversationId,
"downloadUrl": downloadUrl == null ? null : downloadUrl,
"fileTypeResponse": fileTypeResponse == null ? null : fileTypeResponse!.toJson(),
"userChatReplyResponse": userChatReplyResponse == null ? null : userChatReplyResponse!.toJson(),
};

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
class ViewAllAttachmentPage extends StatefulWidget {
int moduleId;
int requestId;
ViewAllAttachmentPage({required this.moduleId, required this.requestId, Key? key}) : super(key: key);
@override
_ViewAllAttachmentPageState createState() {
return _ViewAllAttachmentPageState();
}
}
class _ViewAllAttachmentPageState extends State<ViewAllAttachmentPage> {
@override
void initState() {
super.initState();
Provider.of<ChatProvider>(context, listen: false).viewAllDocuments(widget.moduleId, widget.requestId);
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.neutral100,
appBar: const DefaultAppBar(title: "Documents"),
body: FutureBuilder(
future: Provider.of<ChatProvider>(context, listen: false).viewAllDocuments(widget.moduleId, widget.requestId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
bool isLoading = false;
if (snapshot.connectionState == ConnectionState.waiting) {
isLoading = true;
}
return GridView.builder(
itemCount: 9, //isLoading? 9: 2,
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: context.isTablet() ? 4 : 3, childAspectRatio: 1, crossAxisSpacing: 8, mainAxisSpacing: 8),
itemBuilder: (BuildContext context, int index) {
return Container().toShimmer(context: context, isShow: isLoading);
},
);
}),
);
}
}

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/app_style/sizing.dart';
class UploadingDialog extends StatelessWidget {
const UploadingDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 150.toScreenWidth,
width: 200.toScreenWidth,
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.neutral30,
borderRadius: BorderRadius.circular(20.0),
boxShadow: [AppStyle.boxShadow],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(strokeWidth: 3),
16.height,
Text(
"Uploading...",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColor.textColor(context), height: 35 / 24, letterSpacing: -0.96),
)
],
),
),
);
}
}

@ -7,8 +7,9 @@ import 'package:test_sa/new_views/app_style/app_text_style.dart';
class ASoundPlayer extends StatefulWidget {
final String audio;
final bool showLoading;
const ASoundPlayer({Key? key, required this.audio}) : super(key: key);
const ASoundPlayer({Key? key, required this.audio, this.showLoading = false}) : super(key: key);
@override
_ASoundPlayerState createState() => _ASoundPlayerState();
@ -161,6 +162,7 @@ class _ASoundPlayerState extends State<ASoundPlayer> {
if (_isLocalFile) {
await _audioPlayer.setSourceDeviceFile(_audio);
} else {
// await _audioPlayer.setSource(UrlSource(_audio, headers: widget.headers));
await _audioPlayer.setSourceUrl(_audio);
}
_audioPlayer.seek(const Duration(milliseconds: 0));

Loading…
Cancel
Save