diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e717a951..d1cc3c30 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,13 +37,11 @@ dependencies { implementation(libs.activity) implementation(libs.constraintlayout) - implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:4.9.1") implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("com.google.android.material:material:1.11.0") implementation("androidx.viewpager2:viewpager2:1.1.0") @@ -53,6 +51,11 @@ dependencies { implementation("androidx.camera:camera-view:1.4.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation(libs.swiperefreshlayout) + + implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6") + implementation("io.reactivex.rxjava2:rxjava:2.2.21") + implementation("io.reactivex.rxjava2:rxandroid:2.1.1") + testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java b/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java index f7af97e5..6134d221 100644 --- a/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java +++ b/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java @@ -1,7 +1,7 @@ package com.example.petstoremobile; import android.app.Application; -import com.example.petstoremobile.api.Auth.TokenManager; +import com.example.petstoremobile.api.auth.TokenManager; public class PetStoreApplication extends Application { @Override diff --git a/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index d710d927..6bc65f23 100644 --- a/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -15,8 +15,8 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.Auth.AuthApi; -import com.example.petstoremobile.api.Auth.TokenManager; +import com.example.petstoremobile.api.auth.AuthApi; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.AuthDTO; @@ -64,7 +64,7 @@ public class MainActivity extends AppCompatActivity { //Set click listener for login button btnLogin.setOnClickListener(v -> { - //Get user name and password from text fields + //Get username and password from text fields String username = etUser.getText().toString(); String password = etPassword.getText().toString(); @@ -88,11 +88,33 @@ public class MainActivity extends AppCompatActivity { response.body().getUsername(), response.body().getRole() ); - //go to home activity after login - Intent intent = new Intent(MainActivity.this, HomeActivity.class); - startActivity(intent); - Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); - finish(); + + //fetch user id from api then login to home activity + RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser() + .enqueue(new Callback() { + @Override + public void onResponse(Call call, + Response response) { + if (response.isSuccessful() && response.body() != null) { + TokenManager.getInstance(MainActivity.this) + .saveUserId(response.body().getId()); + } + + Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(MainActivity.this, HomeActivity.class)); + finish(); + } + + @Override + public void onFailure(Call call, + Throwable t) { + Log.e("MainActivity", "Failed to fetch userId", t); + + Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(MainActivity.this, HomeActivity.class)); + finish(); + } + }); } else { Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show(); tvLoginStatus.setText("Login failed"); diff --git a/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java new file mode 100644 index 00000000..0354a1fd --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -0,0 +1,78 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Message; +import java.util.List; + +public class MessageAdapter extends RecyclerView.Adapter { + + private static final int TYPE_SENT = 1; + private static final int TYPE_RECEIVED = 2; + + private final List messages; + private Long currentUserId; + + public MessageAdapter(List messages, Long currentUserId) { + this.messages = messages; + this.currentUserId = currentUserId; + } + + public void setCurrentUserId(Long id) { + this.currentUserId = id; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + Message m = messages.get(position); + if (currentUserId != null && currentUserId.equals(m.getSenderId())) { + return TYPE_SENT; + } + return TYPE_RECEIVED; + } + + @NonNull @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inf = LayoutInflater.from(parent.getContext()); + if (viewType == TYPE_SENT) { + View v = inf.inflate(R.layout.item_message_sent, parent, false); + return new SentHolder(v); + } else { + View v = inf.inflate(R.layout.item_message_received, parent, false); + return new ReceivedHolder(v); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + Message m = messages.get(position); + if (holder instanceof SentHolder) ((SentHolder) holder).bind(m); + if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m); + } + + @Override public int getItemCount() { return messages.size(); } + + static class SentHolder extends RecyclerView.ViewHolder { + TextView tvMessage; + SentHolder(View v) { + super(v); + tvMessage = v.findViewById(R.id.tvMessageContent); // updated + } + void bind(Message m) { tvMessage.setText(m.getContent()); } + } + + static class ReceivedHolder extends RecyclerView.ViewHolder { + TextView tvMessage; + ReceivedHolder(View v) { + super(v); + tvMessage = v.findViewById(R.id.tvMessageContent); // updated + } + void bind(Message m) { tvMessage.setText(m.getContent()); } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/api/ChatApi.java b/app/src/main/java/com/example/petstoremobile/api/ChatApi.java index b5d91cc4..63f5ac51 100644 --- a/app/src/main/java/com/example/petstoremobile/api/ChatApi.java +++ b/app/src/main/java/com/example/petstoremobile/api/ChatApi.java @@ -11,17 +11,13 @@ import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Path; +//api calls to get conversations public interface ChatApi { - @GET("v1/chat/conversations") + @GET("api/v1/chat/conversations") Call> getAllConversations(); - @GET("v1/chat/conversations/{conversationId}") + @GET("api/v1/chat/conversations/{conversationId}") Call getConversationById(@Path("conversationId") Long conversationId); - @GET("v1/chat/conversations/{conversationId}/messages") - Call> getMessages(@Path("conversationId") Long conversationId); - - @POST("v1/chat/conversations/{conversationId}/messages") - Call sendMessage(@Path("conversationId") Long conversationId, @Body MessageDTO message); } \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java index 6f7d2a14..02700075 100644 --- a/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java +++ b/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java @@ -10,11 +10,12 @@ import retrofit2.http.GET; import retrofit2.http.Path; import retrofit2.http.Query; +//api calls to get customers public interface CustomerApi { - @GET("v1/customers") + @GET("api/v1/customers") Call> getAllCustomers(@Query("page") int page, @Query("size") int size); - @GET("v1/customers/{customerId}") + @GET("api/v1/customers/{customerId}") Call getCustomerById(@Path("customerId") Long customerId); } \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/app/src/main/java/com/example/petstoremobile/api/MessageApi.java new file mode 100644 index 00000000..13df781f --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/api/MessageApi.java @@ -0,0 +1,20 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.MessageDTO; +import com.example.petstoremobile.dtos.SendMessageRequest; +import java.util.List; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +//api calls to get and send messages +public interface MessageApi { + + @GET("api/v1/chat/conversations/{id}/messages") + Call> getMessages(@Path("id") Long conversationId); + + @POST("api/v1/chat/conversations/{id}/messages") + Call sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/app/src/main/java/com/example/petstoremobile/api/PetApi.java index b9a5c5b2..35fccfbc 100644 --- a/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -12,28 +12,29 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +//api calls to CRUD pets public interface PetApi { // Get all pets - @GET("v1/pets") + @GET("api/v1/pets") Call> getAllPets( @Query("page") int page, @Query("size") int size ); // Get pet by id - @GET("v1/pets/{id}") + @GET("api/v1/pets/{id}") Call getPetById(@Path("id") Long id); // Create pet - @POST("v1/pets") + @POST("api/v1/pets") Call createPet(@Body PetDTO pet); // Update pet - @PUT("v1/pets/{id}") + @PUT("api/v1/pets/{id}") Call updatePet(@Path("id") Long id, @Body PetDTO pet); // Delete pet - @DELETE("v1/pets/{id}") + @DELETE("api/v1/pets/{id}") Call deletePet(@Path("id") Long id); } diff --git a/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java index ccdcf696..8a7f48bc 100644 --- a/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ b/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -2,18 +2,19 @@ package com.example.petstoremobile.api; import android.content.Context; -import com.example.petstoremobile.api.Auth.AuthApi; -import com.example.petstoremobile.api.Auth.AuthInterceptor; +import com.example.petstoremobile.api.auth.AuthApi; +import com.example.petstoremobile.api.auth.AuthInterceptor; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; +//Retrofit client Used for API calls public class RetrofitClient { //base URL -// public static final String BASE_URL = "http://10.0.2.2:8080/api/"; //for emulator testing - public static final String BASE_URL = "http://10.0.0.200:8080/api/"; //for hardware testing change to computer ip if using hardware to test + public static final String BASE_URL = "http://10.0.2.2:8080"; //for emulator testing +// public static final String BASE_URL = "http://10.0.0.200:8080/"; //for hardware testing private static Retrofit retrofit = null; @@ -62,4 +63,8 @@ public class RetrofitClient { return getClient(context).create(CustomerApi.class); } + public static MessageApi getMessageApi(Context context) { + return getClient(context).create(MessageApi.class); + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java b/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java index 467b8935..a8e4ed32 100644 --- a/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java +++ b/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java @@ -12,27 +12,28 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +//api calls to CRUD services public interface ServiceApi { // Get all services - @GET("v1/services") + @GET("api/v1/services") Call> getAllServices( @Query("page") int page, @Query("size") int size ); // Get service by id - @GET("v1/services/{id}") + @GET("api/v1/services/{id}") Call getServiceById(@Path("id") Long id); // Create service - @POST("v1/services") + @POST("api/v1/services") Call createService(@Body ServiceDTO service); // Update service - @PUT("v1/services/{id}") + @PUT("api/v1/services/{id}") Call updateService(@Path("id") Long id, @Body ServiceDTO service); // Delete service - @DELETE("v1/services/{id}") + @DELETE("api/v1/services/{id}") Call deleteService(@Path("id") Long id); } diff --git a/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java b/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java index beb2ded9..47d4e1e3 100644 --- a/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java +++ b/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java @@ -12,27 +12,28 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +//api calls to CRUD suppliers public interface SupplierApi { // Get all suppliers - @GET("v1/suppliers") + @GET("api/v1/suppliers") Call> getAllSuppliers( @Query("page") int page, @Query("size") int size ); // Get supplier by id - @GET("v1/suppliers/{id}") + @GET("api/v1/suppliers/{id}") Call getSupplierById(@Path("id") Long id); // Create supplier - @POST("v1/suppliers") + @POST("api/v1/suppliers") Call createSupplier(@Body SupplierDTO supplier); // Update supplier - @PUT("v1/suppliers/{id}") + @PUT("api/v1/suppliers/{id}") Call updateSupplier(@Path("id") Long id, @Body SupplierDTO supplier); // Delete supplier - @DELETE("v1/suppliers/{id}") + @DELETE("api/v1/suppliers/{id}") Call deleteSupplier(@Path("id") Long id); } diff --git a/app/src/main/java/com/example/petstoremobile/api/Auth/AuthApi.java b/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java similarity index 51% rename from app/src/main/java/com/example/petstoremobile/api/Auth/AuthApi.java rename to app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java index c8ccdcc9..cf82623f 100644 --- a/app/src/main/java/com/example/petstoremobile/api/Auth/AuthApi.java +++ b/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java @@ -1,13 +1,17 @@ -package com.example.petstoremobile.api.Auth; +package com.example.petstoremobile.api.auth; import com.example.petstoremobile.dtos.AuthDTO; import retrofit2.Call; import retrofit2.http.Body; +import retrofit2.http.GET; import retrofit2.http.POST; -//Api for logging in +//Api for logging in and getting current user public interface AuthApi { - @POST("v1/auth/login") + @POST("api/v1/auth/login") Call login(@Body AuthDTO.LoginRequest loginRequest); + + @GET("api/v1/auth/me") + Call getCurrentUser(); } diff --git a/app/src/main/java/com/example/petstoremobile/api/Auth/AuthInterceptor.java b/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java similarity index 96% rename from app/src/main/java/com/example/petstoremobile/api/Auth/AuthInterceptor.java rename to app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java index 32044184..dd17fffd 100644 --- a/app/src/main/java/com/example/petstoremobile/api/Auth/AuthInterceptor.java +++ b/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java @@ -1,4 +1,4 @@ -package com.example.petstoremobile.api.Auth; +package com.example.petstoremobile.api.auth; import android.content.Context; diff --git a/app/src/main/java/com/example/petstoremobile/api/Auth/TokenManager.java b/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java similarity index 82% rename from app/src/main/java/com/example/petstoremobile/api/Auth/TokenManager.java rename to app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java index 47322763..b0f90508 100644 --- a/app/src/main/java/com/example/petstoremobile/api/Auth/TokenManager.java +++ b/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java @@ -1,4 +1,4 @@ -package com.example.petstoremobile.api.Auth; +package com.example.petstoremobile.api.auth; import android.content.Context; import android.content.SharedPreferences; @@ -9,6 +9,7 @@ public class TokenManager { private static final String USERNAME_KEY = "username"; private static final String ROLE_KEY = "role"; private static final String PREFS_NAME = "auth_prefs"; + private static final String USER_ID_KEY = "user_id"; private static TokenManager instance; private SharedPreferences prefs; @@ -46,6 +47,15 @@ public class TokenManager { return prefs.getString(ROLE_KEY, null); } + public Long getUserId() { + long id = prefs.getLong(USER_ID_KEY, -1L); + return id == -1L ? null : id; + } + + public void saveUserId(Long userId) { + prefs.edit().putLong(USER_ID_KEY, userId).apply(); + } + //Check if logged in public boolean isLoggedIn() { return getToken() != null; diff --git a/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java b/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java index 36a2db35..6aecdbc3 100644 --- a/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java +++ b/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java @@ -25,4 +25,25 @@ public class AuthDTO { public String getUsername() { return username; } public String getRole() { return role; } } + + //Used to get logged in profile + public static class UserResponse { + private Long id; + private String username; + private String email; + private String fullName; + private String avatarUrl; + private String role; + private Long storeId; + private String storeName; + + public Long getId() { return id; } + public String getUsername() { return username; } + public String getEmail() { return email; } + public String getFullName() { return fullName; } + public String getAvatarUrl() { return avatarUrl; } + public String getRole() { return role; } + public Long getStoreId() { return storeId; } + public String getStoreName() { return storeName; } + } } diff --git a/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java b/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java index e1300608..1080b600 100644 --- a/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java +++ b/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java @@ -1,46 +1,44 @@ package com.example.petstoremobile.dtos; +import com.google.gson.annotations.SerializedName; + public class MessageDTO { - private String id; + + @SerializedName("id") + private Long id; + + @SerializedName("conversationId") + private Long conversationId; + + @SerializedName("senderId") + private Long senderId; + + @SerializedName("content") private String content; - private String senderId; - private long timestamp; + + @SerializedName("timestamp") + private String timestamp; + + @SerializedName("isRead") + private Boolean isRead; public MessageDTO() {} - public MessageDTO(String content) { - this.content = content; - } + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } - public String getId() { - return id; - } + public Long getConversationId() { return conversationId; } + public void setConversationId(Long conversationId) { this.conversationId = conversationId; } - public void setId(String id) { - this.id = id; - } + public Long getSenderId() { return senderId; } + public void setSenderId(Long senderId) { this.senderId = senderId; } - public String getContent() { - return content; - } + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } - public void setContent(String content) { - this.content = content; - } + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } - public String getSenderId() { - return senderId; - } - - public void setSenderId(String senderId) { - this.senderId = senderId; - } - - public long getTimestamp() { - return timestamp; - } - - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } + public Boolean getIsRead() { return isRead; } + public void setIsRead(Boolean isRead) { this.isRead = isRead; } } \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java b/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java new file mode 100644 index 00000000..7a521de3 --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java @@ -0,0 +1,13 @@ +package com.example.petstoremobile.dtos; + +public class SendMessageRequest { + + private String content; + + public SendMessageRequest(String content) { + this.content = content; + } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 863aa0eb..b73fdc6b 100644 --- a/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -1,157 +1,407 @@ package com.example.petstoremobile.fragments; import android.os.Bundle; - +import android.util.Log; +import android.view.*; +import android.widget.*; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; +import com.example.petstoremobile.adapters.MessageAdapter; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.ChatApi; import com.example.petstoremobile.api.CustomerApi; +import com.example.petstoremobile.api.MessageApi; import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import com.example.petstoremobile.models.Message; +import com.example.petstoremobile.websocket.StompChatManager; +import java.util.*; import java.util.stream.Collectors; +import retrofit2.*; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; +public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, + StompChatManager.ConversationListener, StompChatManager.ConnectionListener { + private static final String TAG = "ChatFragment"; -public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener { - - private LinearLayout chatContainer; - private EditText etMessage; - private ScrollView scrollView; - private Button btnSend; - private ImageButton hamburger; + // View private DrawerLayout drawerLayout; - private RecyclerView rvChatList; + private RecyclerView rvChatList, rvMessages; + private EditText etMessage; + private Button btnSend; + + // Adapters private ChatAdapter chatAdapter; - private List chatList = new ArrayList<>(); + private MessageAdapter messageAdapter; + + // Data + private final List chatList = new ArrayList<>(); + private final List messageList = new ArrayList<>(); + private final Map customerNames = new HashMap<>(); + + // APIs private ChatApi chatApi; private CustomerApi customerApi; - private Map customerNames = new HashMap<>(); + private MessageApi messageApi; + + // chat + private Long currentUserId; + private Long activeConversationId; + private StompChatManager stompChatManager; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_chat, container, false); chatApi = RetrofitClient.getChatApi(requireContext()); customerApi = RetrofitClient.getCustomerApi(requireContext()); + messageApi = RetrofitClient.getMessageApi(requireContext()); drawerLayout = view.findViewById(R.id.chatDrawerLayout); - hamburger = view.findViewById(R.id.btnHamburger); rvChatList = view.findViewById(R.id.rvChatList); + rvMessages = view.findViewById(R.id.rvMessages); + etMessage = view.findViewById(R.id.etMessage); + btnSend = view.findViewById(R.id.btnSend); - setupRecyclerView(); + ImageButton hamburger = view.findViewById(R.id.btnHamburger); + hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START)); + btnSend.setOnClickListener(v -> sendMessage()); + + setupRecyclerViews(); loadInitialData(); - // Open the local chat drawer when hamburger is clicked - hamburger.setOnClickListener(v -> { - if (drawerLayout != null) { - drawerLayout.openDrawer(GravityCompat.START); - } - }); - return view; } - private void setupRecyclerView() { + private void setupRecyclerViews() { + // Set up Drawer menu to select conversation chatAdapter = new ChatAdapter(chatList, this); rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); rvChatList.setAdapter(chatAdapter); + + // set up RecyclerView for selected chat to show messages + messageAdapter = new MessageAdapter(messageList, null); + LinearLayoutManager lm = new LinearLayoutManager(getContext()); + lm.setStackFromEnd(true); + rvMessages.setLayoutManager(lm); + rvMessages.setAdapter(messageAdapter); + setConversationActive(false); } + //Helper function to load token and user id then connect to websocket private void loadInitialData() { - // Fetch all customers (first page, large size to get many) + TokenManager tm = TokenManager.getInstance(requireContext()); + String token = tm.getToken(); + currentUserId = tm.getUserId(); + String role = tm.getRole(); + + messageAdapter.setCurrentUserId(currentUserId); + + // if token exist then connect to websocket + if (token != null) { + stompChatManager = new StompChatManager(token, role); + stompChatManager.setMessageListener(this); + stompChatManager.setConversationListener(this); + stompChatManager.setConnectionListener(this); + stompChatManager.connect(); + } else { + Log.e(TAG, "No token found"); + } + + loadCustomers(); + } + + //Helper function to load customer names for it to be displayed on drawer menu + private void loadCustomers() { customerApi.getAllCustomers(0, 100).enqueue(new Callback>() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { if (response.isSuccessful() && response.body() != null) { - for (CustomerDTO customer : response.body().getContent()) { - customerNames.put(customer.getCustomerId(), customer.getFullName()); + for (CustomerDTO c : response.body().getContent()) { + customerNames.put(c.getCustomerId(), c.getFullName()); } } - // Then load conversations loadConversations(); } @Override - public void onFailure(Call> call, Throwable t) { - Log.e("ChatFragment", "Error loading customers", t); - loadConversations(); // Try loading conversations anyway + public void onFailure(@NonNull Call> call, + @NonNull Throwable t) { + loadConversations(); } }); } + //helper function to load conversations entities to display with customer names in drawer menu private void loadConversations() { chatApi.getAllConversations().enqueue(new Callback>() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { if (response.isSuccessful() && response.body() != null) { chatList.clear(); - List loadedChats = response.body().stream() + List loaded = response.body().stream() .map(dto -> { - String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - return new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage()); + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + return new Chat(String.valueOf(dto.getId()), + name, dto.getLastMessage(), + dto.getCustomerId(), dto.getStaffId()); }) .collect(Collectors.toList()); - chatList.addAll(loadedChats); + chatList.addAll(loaded); chatAdapter.notifyDataSetChanged(); - } else { - Log.e("ChatFragment", "Failed to load conversations: " + response.message()); + if (activeConversationId == null) { + messageList.clear(); + messageAdapter.notifyDataSetChanged(); + setConversationActive(false); + } } } - @Override - public void onFailure(Call> call, Throwable t) { - Log.e("ChatFragment", "Error loading conversations", t); - Toast.makeText(getContext(), "Error loading chats", Toast.LENGTH_SHORT).show(); + public void onFailure(@NonNull Call> call, + @NonNull Throwable t) { + Log.e(TAG, "Error loading conversations", t); } }); } + // Called when user taps a chat in the drawer + // Loads messages for that chat selected @Override public void onChatClick(Chat chat) { - // Handle chat selection - Toast.makeText(getContext(), "Selected chat: " + chat.getCustomerName(), Toast.LENGTH_SHORT).show(); - - // Close drawer after selection - if (drawerLayout != null) { - drawerLayout.closeDrawer(GravityCompat.START); + activeConversationId = Long.parseLong(chat.getChatId()); + setConversationActive(true); + drawerLayout.closeDrawer(GravityCompat.START); + + if (stompChatManager != null) { + stompChatManager.subscribeToConversation(activeConversationId); } - - // TODO: Load actual messages for the selected chat + + loadMessageHistory(activeConversationId); } -} \ No newline at end of file + + //helper function to load messages for selected chat + private void loadMessageHistory(Long conversationId) { + messageApi.getMessages(conversationId).enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + messageList.clear(); + for (MessageDTO dto : response.body()) { + messageList.add(dtoToModel(dto)); + } + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); + } + } + @Override + public void onFailure(@NonNull Call> call, + @NonNull Throwable t) { + Log.e(TAG, "Error loading messages", t); + } + }); + } + + //Helper function to send a message to the chat + private void sendMessage() { + //check if a chat is selected + if (activeConversationId == null) return; + + //get the message from text field + String text = etMessage.getText().toString().trim(); + if (text.isEmpty()) return; + + //clear text field after sending + etMessage.setText(""); + + //calls api to send the message + messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, + @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + messageList.add(dtoToModel(response.body())); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + loadConversations(); + } + } + @Override + public void onFailure(@NonNull Call call, + @NonNull Throwable t) { + Log.e(TAG, "Send failed", t); + } + }); + } + + // When a message is received updates the chat preview + @Override + public void onMessageReceived(MessageDTO dto) { + //if there is no active selected conversation or the message received is for another chat, then just update the preview of last message + if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) { + updateConversationPreview(dto.getConversationId(), dto.getContent()); + return; + } + updateConversationPreview(dto.getConversationId(), dto.getContent()); + + if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return; + + //else add the message to the active chat if it's not from the current user + messageList.add(dtoToModel(dto)); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + } + + // When a new conversation is added, updates the chat preview + @Override + public void onConversationUpdated(ConversationDTO dto) { + boolean updated = false; + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + + for (int i = 0; i < chatList.size(); i++) { + Chat existing = chatList.get(i); + if (existing.getChatId().equals(String.valueOf(dto.getId()))) { + chatList.set(i, new Chat( + String.valueOf(dto.getId()), + name, + dto.getLastMessage(), + dto.getCustomerId(), + dto.getStaffId() + )); + chatAdapter.notifyItemChanged(i); + updated = true; + break; + } + } + + if (!updated) { + chatList.add(0, new Chat( + String.valueOf(dto.getId()), + name, + dto.getLastMessage(), + dto.getCustomerId(), + dto.getStaffId() + )); + chatAdapter.notifyItemInserted(0); + } + + if (activeConversationId != null && activeConversationId.equals(dto.getId())) { + setConversationActive(true); + } + } + + @Override + public void onSocketOpened() { + if (!isAdded()) { + return; + } + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + } + + @Override + public void onSocketClosed() { + if (!isAdded()) { + return; + } + loadConversations(); + } + + @Override + public void onSocketError() { + if (!isAdded()) { + return; + } + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + } + + // Helper function to convert DTO to message + private Message dtoToModel(MessageDTO dto) { + Message m = new Message(); + m.setId(dto.getId()); + m.setConversationId(dto.getConversationId()); + m.setSenderId(dto.getSenderId()); + m.setContent(dto.getContent()); + m.setTimestamp(dto.getTimestamp()); + m.setIsRead(dto.getIsRead()); + return m; + } + + //Helper function to scroll to bottom of the chat + private void scrollToBottom() { + if (!messageList.isEmpty()) { + rvMessages.post(() -> + rvMessages.smoothScrollToPosition(messageList.size() - 1)); + } + } + + // Helper function to update the chat preview last message + private void updateConversationPreview(Long conversationId, String lastMessage) { + if (conversationId == null) { + return; + } + for (int i = 0; i < chatList.size(); i++) { + Chat existing = chatList.get(i); + if (existing.getChatId().equals(String.valueOf(conversationId))) { + Chat updated = new Chat( + existing.getChatId(), + existing.getCustomerName(), + lastMessage, + existing.getCustomerId(), + existing.getStaffId() + ); + chatList.set(i, updated); + chatAdapter.notifyItemChanged(i); + return; + } + } + } + + //Helper function to enable or disable the send button when there is no active chat + private void setConversationActive(boolean active) { + btnSend.setEnabled(active); + etMessage.setEnabled(active); + if (!active) { + activeConversationId = null; + if (stompChatManager != null) { + stompChatManager.clearConversationSubscription(); + } + messageList.clear(); + messageAdapter.notifyDataSetChanged(); + etMessage.setText(""); + etMessage.setHint("Select a chat to start messaging"); + } else { + etMessage.setHint("Type a message..."); + } + } + + // When fragment is destroyed, disconnect from websocket + @Override + public void onDestroyView() { + super.onDestroyView(); + if (stompChatManager != null) stompChatManager.disconnect(); + } +} diff --git a/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index 92b6e9ff..2bb92e4c 100644 --- a/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -26,7 +26,7 @@ import android.widget.TextView; import com.example.petstoremobile.R; import com.example.petstoremobile.activities.MainActivity; -import com.example.petstoremobile.api.Auth.TokenManager; +import com.example.petstoremobile.api.auth.TokenManager; import java.io.File; @@ -150,6 +150,7 @@ public class ProfileFragment extends Fragment { } }) .show(); + //TODO: UPDATE PHOTO IN DATABASE }); //Edit email button diff --git a/app/src/main/java/com/example/petstoremobile/models/Chat.java b/app/src/main/java/com/example/petstoremobile/models/Chat.java index bbc5ac26..f3a9a4eb 100644 --- a/app/src/main/java/com/example/petstoremobile/models/Chat.java +++ b/app/src/main/java/com/example/petstoremobile/models/Chat.java @@ -4,11 +4,15 @@ public class Chat { private String chatId; private String customerName; private String lastMessage; + private Long customerId; + private Long staffId; - public Chat(String chatId, String customerName, String lastMessage) { + public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) { this.chatId = chatId; this.customerName = customerName; this.lastMessage = lastMessage; + this.customerId = customerId; + this.staffId = staffId; } public String getChatId() { @@ -22,4 +26,12 @@ public class Chat { public String getLastMessage() { return lastMessage; } + + public Long getCustomerId() { + return customerId; + } + + public Long getStaffId() { + return staffId; + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/models/Message.java b/app/src/main/java/com/example/petstoremobile/models/Message.java new file mode 100644 index 00000000..18ec549a --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/models/Message.java @@ -0,0 +1,36 @@ +package com.example.petstoremobile.models; + +public class Message { + private Long id; + private Long conversationId; + private Long senderId; + private String content; + private String timestamp; + private Boolean isRead; + + public Message() {} + + public Message(Long conversationId, Long senderId, String content) { + this.conversationId = conversationId; + this.senderId = senderId; + this.content = content; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Long getConversationId() { return conversationId; } + public void setConversationId(Long conversationId) { this.conversationId = conversationId; } + + public Long getSenderId() { return senderId; } + public void setSenderId(Long senderId) { this.senderId = senderId; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } + + public Boolean getIsRead() { return isRead; } + public void setIsRead(Boolean isRead) { this.isRead = isRead; } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java b/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java new file mode 100644 index 00000000..af4264db --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java @@ -0,0 +1,295 @@ +package com.example.petstoremobile.websocket; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.MessageDTO; +import com.google.gson.Gson; +import java.util.HashMap; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import ua.naiksoftware.stomp.Stomp; +import ua.naiksoftware.stomp.StompClient; +import ua.naiksoftware.stomp.dto.StompHeader; + +//Used to handle the websocket connection for the chat +public class StompChatManager { + + private static final String TAG = "StompChatManager"; + + //Interface for when a message is received + public interface MessageListener { + void onMessageReceived(MessageDTO message); + } + + //Interface for when a conversation is created or updated + public interface ConversationListener { + void onConversationUpdated(ConversationDTO conversation); + } + + //Interface for when the websocket connection is opened, closed, or has an error + public interface ConnectionListener { + void onSocketOpened(); + void onSocketClosed(); + void onSocketError(); + } + + private StompClient stompClient; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private Disposable topicDisposable; + private Disposable conversationsDisposable; + private Disposable userConversationsDisposable; + private Disposable errorQueueDisposable; + private final Gson gson = new Gson(); + private final Handler reconnectHandler = new Handler(Looper.getMainLooper()); + private MessageListener messageListener; + private ConversationListener conversationListener; + private ConnectionListener connectionListener; + private final String authToken; + private final String role; + private boolean isConnected; + private boolean isConnecting; + private boolean manualDisconnect; + private Long pendingConversationId; + + public StompChatManager(String authToken, String role) { + this.authToken = authToken; + this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT); + } + + public void setMessageListener(MessageListener listener) { + this.messageListener = listener; + } + + public void setConversationListener(ConversationListener listener) { + this.conversationListener = listener; + } + + public void setConnectionListener(ConnectionListener listener) { + this.connectionListener = listener; + } + + // Set up a stomp connection + public void connect() { + if (authToken == null || authToken.isBlank()) { + Log.e(TAG, "Cannot connect websocket without token"); + return; + } + if (isConnected || isConnecting) { + return; + } + manualDisconnect = false; + isConnecting = true; + reconnectHandler.removeCallbacksAndMessages(null); + String webSocketUrl = buildWebSocketUrl(); + + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + authToken); + + stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers); + + compositeDisposable.add( + stompClient.lifecycle() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(event -> { + switch (event.getType()) { + case OPENED: + isConnected = true; + isConnecting = false; + Log.d(TAG, "WebSocket opened"); + if (connectionListener != null) { + connectionListener.onSocketOpened(); + } + subscribeToErrorQueue(); + subscribeToConversationFeeds(); + if (pendingConversationId != null) { + subscribeToTopic(pendingConversationId); + } + break; + case CLOSED: + isConnected = false; + isConnecting = false; + Log.d(TAG, "WebSocket closed"); + if (connectionListener != null) { + connectionListener.onSocketClosed(); + } + scheduleReconnect(); + break; + case ERROR: + isConnected = false; + isConnecting = false; + Log.e(TAG, "WebSocket error: " + event.getException()); + if (connectionListener != null) { + connectionListener.onSocketError(); + } + scheduleReconnect(); + break; + } + }) + ); + + stompClient.connect(Collections.singletonList( + new StompHeader("Authorization", "Bearer " + authToken) + )); + } + + // Subscribes to updates for a specific conversation + public void subscribeToConversation(Long conversationId) { + pendingConversationId = conversationId; + if (!isConnected || stompClient == null) { + Log.d(TAG, "Delaying subscription until socket opens for conversation " + conversationId); + connect(); + return; + } + subscribeToTopic(conversationId); + } + + // Clears the current conversation subscription + public void clearConversationSubscription() { + pendingConversationId = null; + if (topicDisposable != null && !topicDisposable.isDisposed()) { + topicDisposable.dispose(); + topicDisposable = null; + } + } + + //helper function to subscribe to a specific conversation topic + private void subscribeToTopic(Long conversationId) { + if (topicDisposable != null && !topicDisposable.isDisposed()) { + topicDisposable.dispose(); + } + + String topic = "/topic/chat/conversations/" + conversationId; + Log.d(TAG, "Subscribing to topic " + topic); + + topicDisposable = stompClient.topic(topic) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> { + MessageDTO message = gson.fromJson( + stompMessage.getPayload(), MessageDTO.class); + if (messageListener != null) { + messageListener.onMessageReceived(message); + } + }, + throwable -> Log.e(TAG, "Topic error", throwable) + ); + + compositeDisposable.add(topicDisposable); + } + + // Listens for conversation updates and refresh the chat list + private void subscribeToConversationFeeds() { + if (conversationsDisposable != null && !conversationsDisposable.isDisposed()) { + conversationsDisposable.dispose(); + } + if (userConversationsDisposable != null && !userConversationsDisposable.isDisposed()) { + userConversationsDisposable.dispose(); + } + + userConversationsDisposable = stompClient.topic("/user/queue/chat/conversations") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> { + Log.d(TAG, "Queue conversation update: " + stompMessage.getPayload()); + ConversationDTO conversation = gson.fromJson( + stompMessage.getPayload(), ConversationDTO.class); + if (conversationListener != null) { + conversationListener.onConversationUpdated(conversation); + } + }, + throwable -> Log.e(TAG, "Conversation queue error", throwable) + ); + + compositeDisposable.add(userConversationsDisposable); + + if (isCustomer()) { + return; + } + + conversationsDisposable = stompClient.topic("/topic/chat/conversations") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> { + Log.d(TAG, "Broadcast conversation update: " + stompMessage.getPayload()); + ConversationDTO conversation = gson.fromJson( + stompMessage.getPayload(), ConversationDTO.class); + if (conversationListener != null) { + conversationListener.onConversationUpdated(conversation); + } + }, + throwable -> Log.e(TAG, "Conversation topic error", throwable) + ); + + compositeDisposable.add(conversationsDisposable); + } + + // Log any error from stomp + private void subscribeToErrorQueue() { + if (errorQueueDisposable != null && !errorQueueDisposable.isDisposed()) { + errorQueueDisposable.dispose(); + } + + errorQueueDisposable = stompClient.topic("/user/queue/chat/errors") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> Log.e(TAG, "WebSocket queue error payload: " + stompMessage.getPayload()), + throwable -> Log.e(TAG, "WebSocket error queue subscription failed", throwable) + ); + + compositeDisposable.add(errorQueueDisposable); + } + + // Disconnects the stomp connection + public void disconnect() { + manualDisconnect = true; + isConnected = false; + isConnecting = false; + pendingConversationId = null; + reconnectHandler.removeCallbacksAndMessages(null); + compositeDisposable.clear(); + if (stompClient != null) { + stompClient.disconnect(); + } + } + + // Make the URL for the websocket connection + private String buildWebSocketUrl() { + String baseUrl = RetrofitClient.BASE_URL.endsWith("/") + ? RetrofitClient.BASE_URL.substring(0, RetrofitClient.BASE_URL.length() - 1) + : RetrofitClient.BASE_URL; + if (baseUrl.startsWith("https://")) { + return "wss://" + baseUrl.substring("https://".length()) + "/ws/chat"; + } + if (baseUrl.startsWith("http://")) { + return "ws://" + baseUrl.substring("http://".length()) + "/ws/chat"; + } + return baseUrl + "/ws/chat"; + } + + // Helper to check if the current user is a customer + private boolean isCustomer() { + return "CUSTOMER".equals(role); + } + + // if connection drops, try to reconnect after 1 second + private void scheduleReconnect() { + if (manualDisconnect) { + return; + } + reconnectHandler.removeCallbacksAndMessages(null); + reconnectHandler.postDelayed(this::connect, 1000); + } +} diff --git a/app/src/main/res/drawable/bg_message_received.xml b/app/src/main/res/drawable/bg_message_received.xml new file mode 100644 index 00000000..7cc459d5 --- /dev/null +++ b/app/src/main/res/drawable/bg_message_received.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_message_sent.xml b/app/src/main/res/drawable/bg_message_sent.xml new file mode 100644 index 00000000..a2013587 --- /dev/null +++ b/app/src/main/res/drawable/bg_message_sent.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml index 839337e5..e56e283f 100644 --- a/app/src/main/res/layout/fragment_chat.xml +++ b/app/src/main/res/layout/fragment_chat.xml @@ -37,21 +37,13 @@ - - - - - + android:padding="8dp" + android:clipToPadding="false" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_sent.xml b/app/src/main/res/layout/item_message_sent.xml new file mode 100644 index 00000000..ab99c033 --- /dev/null +++ b/app/src/main/res/layout/item_message_sent.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 5524edf8..d198919e 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -4,8 +4,28 @@ @color/primary_dark @color/primary_medium @color/accent_coral - @color/background_grey + @color/primary_dark @style/Widget.App.EditText @style/Widget.App.Spinner + @style/ThemeOverlay.App.MaterialAlertDialog + @style/Theme.App.AlertDialog + + + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 94cfd02a..0a2384fb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,10 +19,10 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://jitpack.io") } } } rootProject.name = "PetStoreMobile" include(":app") - \ No newline at end of file