From fea01ba8ecb76a13ecc6d1ac7ede4e846c38317b Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:47:43 -0600 Subject: [PATCH] made chat more user frendly --- .../fragments/ChatFragment.java | 117 +++++++++++++----- .../example/petstoremobile/models/Chat.java | 8 +- .../viewmodels/ChatListViewModel.java | 49 ++++++-- .../app/src/main/res/layout/fragment_chat.xml | 103 ++++++++++++--- 4 files changed, 218 insertions(+), 59 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 8ec8252a..5731b9e9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -10,11 +10,18 @@ import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; -import android.view.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; + import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -22,6 +29,7 @@ import androidx.core.view.GravityCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; + import com.bumptech.glide.Glide; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; @@ -45,7 +53,8 @@ import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -65,10 +74,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private FragmentChatBinding binding; private ChatListViewModel viewModel; - private ChatAdapter chatAdapter; + private ChatAdapter activeChatAdapter; + private ChatAdapter closedChatAdapter; private MessageAdapter messageAdapter; - private final List chatList = new ArrayList<>(); + private final List activeChatList = new ArrayList<>(); + private final List closedChatList = new ArrayList<>(); private final List messageList = new ArrayList<>(); private Uri pendingAttachmentUri; @@ -82,7 +93,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ChatListViewModel.class); + viewModel = new ViewModelProvider(requireActivity()).get(ChatListViewModel.class); attachmentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -117,6 +128,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); + setupDrawerToggles(); setupRecyclerViews(); observeViewModel(); loadInitialData(); @@ -124,10 +136,36 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return binding.getRoot(); } + private void setupDrawerToggles() { + binding.headerActiveChats.setOnClickListener(v -> { + if (binding.rvActiveChats.getVisibility() == View.VISIBLE) { + binding.rvActiveChats.setVisibility(View.GONE); + binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_down_float); + } else { + binding.rvActiveChats.setVisibility(View.VISIBLE); + binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_up_float); + } + }); + + binding.headerClosedChats.setOnClickListener(v -> { + if (binding.rvClosedChats.getVisibility() == View.VISIBLE) { + binding.rvClosedChats.setVisibility(View.GONE); + binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_down_float); + } else { + binding.rvClosedChats.setVisibility(View.VISIBLE); + binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_up_float); + } + }); + } + private void setupRecyclerViews() { - chatAdapter = new ChatAdapter(chatList, this); - binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); - binding.rvChatList.setAdapter(chatAdapter); + activeChatAdapter = new ChatAdapter(activeChatList, this); + binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvActiveChats.setAdapter(activeChatAdapter); + + closedChatAdapter = new ChatAdapter(closedChatList, this); + binding.rvClosedChats.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvClosedChats.setAdapter(closedChatAdapter); messageAdapter = new MessageAdapter(messageList, null); messageAdapter.setBaseUrl(baseUrl); @@ -144,7 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis lm.setStackFromEnd(true); binding.rvMessages.setLayoutManager(lm); binding.rvMessages.setAdapter(messageAdapter); - setConversationActive(false); + setConversationActive(false, null); } private void showFullScreenImage(Message message) { @@ -226,19 +264,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } private void observeViewModel() { - viewModel.getChatList().observe(getViewLifecycleOwner(), list -> { - chatList.clear(); - chatList.addAll(list); - chatAdapter.notifyDataSetChanged(); + viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> { + activeChatList.clear(); + activeChatList.addAll(list); + activeChatAdapter.notifyDataSetChanged(); + updateTitleAndStateIfActive(list); + }); - if (activeConversationId != null) { - for (Chat chat : list) { - if (chat.getChatId().equals(String.valueOf(activeConversationId))) { - binding.tvChatTitle.setText(chat.getCustomerName()); - break; - } - } - } + viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> { + closedChatList.clear(); + closedChatList.addAll(list); + closedChatAdapter.notifyDataSetChanged(); + updateTitleAndStateIfActive(list); }); viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { @@ -253,6 +290,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + private void updateTitleAndStateIfActive(List list) { + if (activeConversationId != null) { + for (Chat chat : list) { + if (chat.getChatId().equals(String.valueOf(activeConversationId))) { + binding.tvChatTitle.setText(chat.getCustomerName()); + setConversationActive(true, chat.getStatus()); + break; + } + } + } + } + private void loadInitialData() { String token = tokenManager.getToken(); Long currentUserId = tokenManager.getUserId(); @@ -274,21 +323,29 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) { activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); getActivity().getIntent().removeExtra("conversation_id"); + } else { + // Restore last active conversation if any + activeConversationId = viewModel.getLastActiveConversationId(); } viewModel.loadCustomers(); + // if (activeConversationId != null) { - setConversationActive(true); + // Re-subscribe and load history if there is an active conversation ID if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); viewModel.loadMessageHistory(activeConversationId); + } else { + setConversationActive(false, null); } } @Override public void onChatClick(Chat chat) { activeConversationId = Long.parseLong(chat.getChatId()); - setConversationActive(true); + viewModel.setLastActiveConversationId(activeConversationId); + + setConversationActive(true, chat.getStatus()); binding.tvChatTitle.setText(chat.getCustomerName()); binding.chatDrawerLayout.closeDrawer(GravityCompat.START); @@ -377,8 +434,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), "")); - // Re-load coversations to get correct names if needed or just let local update handle it - viewModel.loadConversations(); }); } @@ -387,7 +442,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis requireActivity().runOnUiThread(() -> { viewModel.updateConversationLocally(dto); if (activeConversationId != null && activeConversationId.equals(dto.getId())) { - setConversationActive(true); + setConversationActive(true, dto.getStatus()); binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId())); } }); @@ -424,8 +479,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - private void setConversationActive(boolean active) { - UIUtils.setViewsEnabled(active, binding.btnSend, binding.etMessage, binding.btnAttach); + private void setConversationActive(boolean active, String status) { + boolean isClosed = "CLOSED".equalsIgnoreCase(status); + UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach); + if (!active) { activeConversationId = null; ChatNotificationService.activeConversationIdInUi = null; @@ -437,7 +494,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.etMessage.setText(""); binding.etMessage.setHint("Select a chat to start messaging"); } else { - binding.etMessage.setHint("Type a message..."); + binding.etMessage.setHint(isClosed ? "This chat is closed" : "Type a message..."); ChatNotificationService.activeConversationIdInUi = activeConversationId; } } @@ -449,4 +506,4 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ChatNotificationService.activeConversationIdInUi = null; if (stompChatManager != null) stompChatManager.disconnect(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java index f3a9a4eb..a519093c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java @@ -6,13 +6,15 @@ public class Chat { private String lastMessage; private Long customerId; private Long staffId; + private String status; - public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) { + public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId, String status) { this.chatId = chatId; this.customerName = customerName; this.lastMessage = lastMessage; this.customerId = customerId; this.staffId = staffId; + this.status = status; } public String getChatId() { @@ -34,4 +36,8 @@ public class Chat { public Long getStaffId() { return staffId; } + + public String getStatus() { + return status; + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java index 1dc5f716..6aa64694 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -32,10 +33,13 @@ public class ChatListViewModel extends ViewModel { private final ChatRepository chatRepository; private final CustomerRepository customerRepository; - private final MutableLiveData> chatList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> activeChats = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> closedChats = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> messageList = new MutableLiveData<>(new ArrayList<>()); private final Map customerNames = new HashMap<>(); private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private Long lastActiveConversationId = null; @Inject public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) { @@ -43,10 +47,19 @@ public class ChatListViewModel extends ViewModel { this.customerRepository = customerRepository; } - public LiveData> getChatList() { return chatList; } + public LiveData> getActiveChats() { return activeChats; } + public LiveData> getClosedChats() { return closedChats; } public LiveData> getMessageList() { return messageList; } public LiveData getIsLoading() { return isLoading; } + public Long getLastActiveConversationId() { + return lastActiveConversationId; + } + + public void setLastActiveConversationId(Long conversationId) { + this.lastActiveConversationId = conversationId; + } + public void loadCustomers() { customerRepository.getAllCustomers(0, 100).observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -62,12 +75,19 @@ public class ChatListViewModel extends ViewModel { isLoading.setValue(true); chatRepository.getAllConversations().observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - List chats = new ArrayList<>(); + List active = new ArrayList<>(); + List closed = new ArrayList<>(); for (ConversationDTO dto : resource.data) { String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - chats.add(new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + Chat chat = new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus()); + if ("CLOSED".equalsIgnoreCase(dto.getStatus())) { + closed.add(chat); + } else { + active.add(chat); + } } - chatList.setValue(chats); + activeChats.setValue(active); + closedChats.setValue(closed); isLoading.setValue(false); } else if (resource != null && resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -106,21 +126,24 @@ public class ChatListViewModel extends ViewModel { } public void updateConversationLocally(ConversationDTO dto) { - List current = new ArrayList<>(chatList.getValue()); - boolean updated = false; - String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + updateList(activeChats, dto); + updateList(closedChats, dto); + loadConversations(); + } + + private void updateList(MutableLiveData> liveData, ConversationDTO dto) { + List current = new ArrayList<>(liveData.getValue()); + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + boolean updated = false; for (int i = 0; i < current.size(); i++) { if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) { - current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus())); updated = true; break; } } - if (!updated) { - current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); - } - chatList.setValue(current); + if (updated) liveData.setValue(current); } private Message dtoToModel(MessageDTO dto) { diff --git a/android/app/src/main/res/layout/fragment_chat.xml b/android/app/src/main/res/layout/fragment_chat.xml index 4101f774..f3571862 100644 --- a/android/app/src/main/res/layout/fragment_chat.xml +++ b/android/app/src/main/res/layout/fragment_chat.xml @@ -1,6 +1,7 @@ @@ -125,29 +126,101 @@ - + android:background="@color/primary_dark"> - + android:orientation="vertical" + android:paddingTop="24dp"> - + - + + + + + + + + + + + + + + + + + + \ No newline at end of file