Added chat implementation with websockets
This commit is contained in:
@@ -37,13 +37,11 @@ dependencies {
|
|||||||
implementation(libs.activity)
|
implementation(libs.activity)
|
||||||
implementation(libs.constraintlayout)
|
implementation(libs.constraintlayout)
|
||||||
|
|
||||||
|
|
||||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:converter-gson: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:logging-interceptor:4.9.1")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
|
||||||
|
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||||
|
|
||||||
@@ -53,6 +51,11 @@ dependencies {
|
|||||||
implementation("androidx.camera:camera-view:1.4.0")
|
implementation("androidx.camera:camera-view:1.4.0")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation(libs.swiperefreshlayout)
|
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)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.ext.junit)
|
androidTestImplementation(libs.ext.junit)
|
||||||
androidTestImplementation(libs.espresso.core)
|
androidTestImplementation(libs.espresso.core)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.example.petstoremobile;
|
package com.example.petstoremobile;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import com.example.petstoremobile.api.Auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
|
|
||||||
public class PetStoreApplication extends Application {
|
public class PetStoreApplication extends Application {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import androidx.core.view.ViewCompat;
|
|||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.api.Auth.AuthApi;
|
import com.example.petstoremobile.api.auth.AuthApi;
|
||||||
import com.example.petstoremobile.api.Auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.api.RetrofitClient;
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
import com.example.petstoremobile.dtos.AuthDTO;
|
import com.example.petstoremobile.dtos.AuthDTO;
|
||||||
|
|
||||||
@@ -88,11 +88,33 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
response.body().getUsername(),
|
response.body().getUsername(),
|
||||||
response.body().getRole()
|
response.body().getRole()
|
||||||
);
|
);
|
||||||
//go to home activity after login
|
|
||||||
Intent intent = new Intent(MainActivity.this, HomeActivity.class);
|
//fetch user id from api then login to home activity
|
||||||
startActivity(intent);
|
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
|
||||||
|
.enqueue(new Callback<AuthDTO.UserResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<AuthDTO.UserResponse> call,
|
||||||
|
Response<AuthDTO.UserResponse> 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();
|
Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show();
|
||||||
|
startActivity(new Intent(MainActivity.this, HomeActivity.class));
|
||||||
finish();
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<AuthDTO.UserResponse> 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 {
|
} else {
|
||||||
Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
|
Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
|
||||||
tvLoginStatus.setText("Login failed");
|
tvLoginStatus.setText("Login failed");
|
||||||
|
|||||||
@@ -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<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
private static final int TYPE_SENT = 1;
|
||||||
|
private static final int TYPE_RECEIVED = 2;
|
||||||
|
|
||||||
|
private final List<Message> messages;
|
||||||
|
private Long currentUserId;
|
||||||
|
|
||||||
|
public MessageAdapter(List<Message> 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()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,17 +11,13 @@ import retrofit2.http.GET;
|
|||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
import retrofit2.http.Path;
|
import retrofit2.http.Path;
|
||||||
|
|
||||||
|
//api calls to get conversations
|
||||||
public interface ChatApi {
|
public interface ChatApi {
|
||||||
|
|
||||||
@GET("v1/chat/conversations")
|
@GET("api/v1/chat/conversations")
|
||||||
Call<List<ConversationDTO>> getAllConversations();
|
Call<List<ConversationDTO>> getAllConversations();
|
||||||
|
|
||||||
@GET("v1/chat/conversations/{conversationId}")
|
@GET("api/v1/chat/conversations/{conversationId}")
|
||||||
Call<ConversationDTO> getConversationById(@Path("conversationId") Long conversationId);
|
Call<ConversationDTO> getConversationById(@Path("conversationId") Long conversationId);
|
||||||
|
|
||||||
@GET("v1/chat/conversations/{conversationId}/messages")
|
|
||||||
Call<List<MessageDTO>> getMessages(@Path("conversationId") Long conversationId);
|
|
||||||
|
|
||||||
@POST("v1/chat/conversations/{conversationId}/messages")
|
|
||||||
Call<MessageDTO> sendMessage(@Path("conversationId") Long conversationId, @Body MessageDTO message);
|
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,12 @@ import retrofit2.http.GET;
|
|||||||
import retrofit2.http.Path;
|
import retrofit2.http.Path;
|
||||||
import retrofit2.http.Query;
|
import retrofit2.http.Query;
|
||||||
|
|
||||||
|
//api calls to get customers
|
||||||
public interface CustomerApi {
|
public interface CustomerApi {
|
||||||
|
|
||||||
@GET("v1/customers")
|
@GET("api/v1/customers")
|
||||||
Call<PageResponse<CustomerDTO>> getAllCustomers(@Query("page") int page, @Query("size") int size);
|
Call<PageResponse<CustomerDTO>> getAllCustomers(@Query("page") int page, @Query("size") int size);
|
||||||
|
|
||||||
@GET("v1/customers/{customerId}")
|
@GET("api/v1/customers/{customerId}")
|
||||||
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
|
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
|
||||||
}
|
}
|
||||||
@@ -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<List<MessageDTO>> getMessages(@Path("id") Long conversationId);
|
||||||
|
|
||||||
|
@POST("api/v1/chat/conversations/{id}/messages")
|
||||||
|
Call<MessageDTO> sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
|
||||||
|
}
|
||||||
@@ -12,28 +12,29 @@ import retrofit2.http.PUT;
|
|||||||
import retrofit2.http.Path;
|
import retrofit2.http.Path;
|
||||||
import retrofit2.http.Query;
|
import retrofit2.http.Query;
|
||||||
|
|
||||||
|
//api calls to CRUD pets
|
||||||
public interface PetApi {
|
public interface PetApi {
|
||||||
// Get all pets
|
// Get all pets
|
||||||
@GET("v1/pets")
|
@GET("api/v1/pets")
|
||||||
Call<PageResponse<PetDTO>> getAllPets(
|
Call<PageResponse<PetDTO>> getAllPets(
|
||||||
@Query("page") int page,
|
@Query("page") int page,
|
||||||
@Query("size") int size
|
@Query("size") int size
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get pet by id
|
// Get pet by id
|
||||||
@GET("v1/pets/{id}")
|
@GET("api/v1/pets/{id}")
|
||||||
Call<PetDTO> getPetById(@Path("id") Long id);
|
Call<PetDTO> getPetById(@Path("id") Long id);
|
||||||
|
|
||||||
// Create pet
|
// Create pet
|
||||||
@POST("v1/pets")
|
@POST("api/v1/pets")
|
||||||
Call<PetDTO> createPet(@Body PetDTO pet);
|
Call<PetDTO> createPet(@Body PetDTO pet);
|
||||||
|
|
||||||
// Update pet
|
// Update pet
|
||||||
@PUT("v1/pets/{id}")
|
@PUT("api/v1/pets/{id}")
|
||||||
Call<PetDTO> updatePet(@Path("id") Long id, @Body PetDTO pet);
|
Call<PetDTO> updatePet(@Path("id") Long id, @Body PetDTO pet);
|
||||||
|
|
||||||
// Delete pet
|
// Delete pet
|
||||||
@DELETE("v1/pets/{id}")
|
@DELETE("api/v1/pets/{id}")
|
||||||
Call<Void> deletePet(@Path("id") Long id);
|
Call<Void> deletePet(@Path("id") Long id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ package com.example.petstoremobile.api;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import com.example.petstoremobile.api.Auth.AuthApi;
|
import com.example.petstoremobile.api.auth.AuthApi;
|
||||||
import com.example.petstoremobile.api.Auth.AuthInterceptor;
|
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.logging.HttpLoggingInterceptor;
|
import okhttp3.logging.HttpLoggingInterceptor;
|
||||||
import retrofit2.Retrofit;
|
import retrofit2.Retrofit;
|
||||||
import retrofit2.converter.gson.GsonConverterFactory;
|
import retrofit2.converter.gson.GsonConverterFactory;
|
||||||
|
|
||||||
|
//Retrofit client Used for API calls
|
||||||
public class RetrofitClient {
|
public class RetrofitClient {
|
||||||
//base URL
|
//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.2.2:8080"; //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.0.200:8080/"; //for hardware testing
|
||||||
|
|
||||||
private static Retrofit retrofit = null;
|
private static Retrofit retrofit = null;
|
||||||
|
|
||||||
@@ -62,4 +63,8 @@ public class RetrofitClient {
|
|||||||
return getClient(context).create(CustomerApi.class);
|
return getClient(context).create(CustomerApi.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static MessageApi getMessageApi(Context context) {
|
||||||
|
return getClient(context).create(MessageApi.class);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,27 +12,28 @@ import retrofit2.http.PUT;
|
|||||||
import retrofit2.http.Path;
|
import retrofit2.http.Path;
|
||||||
import retrofit2.http.Query;
|
import retrofit2.http.Query;
|
||||||
|
|
||||||
|
//api calls to CRUD services
|
||||||
public interface ServiceApi {
|
public interface ServiceApi {
|
||||||
// Get all services
|
// Get all services
|
||||||
@GET("v1/services")
|
@GET("api/v1/services")
|
||||||
Call<PageResponse<ServiceDTO>> getAllServices(
|
Call<PageResponse<ServiceDTO>> getAllServices(
|
||||||
@Query("page") int page,
|
@Query("page") int page,
|
||||||
@Query("size") int size
|
@Query("size") int size
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get service by id
|
// Get service by id
|
||||||
@GET("v1/services/{id}")
|
@GET("api/v1/services/{id}")
|
||||||
Call<ServiceDTO> getServiceById(@Path("id") Long id);
|
Call<ServiceDTO> getServiceById(@Path("id") Long id);
|
||||||
|
|
||||||
// Create service
|
// Create service
|
||||||
@POST("v1/services")
|
@POST("api/v1/services")
|
||||||
Call<ServiceDTO> createService(@Body ServiceDTO service);
|
Call<ServiceDTO> createService(@Body ServiceDTO service);
|
||||||
|
|
||||||
// Update service
|
// Update service
|
||||||
@PUT("v1/services/{id}")
|
@PUT("api/v1/services/{id}")
|
||||||
Call<ServiceDTO> updateService(@Path("id") Long id, @Body ServiceDTO service);
|
Call<ServiceDTO> updateService(@Path("id") Long id, @Body ServiceDTO service);
|
||||||
|
|
||||||
// Delete service
|
// Delete service
|
||||||
@DELETE("v1/services/{id}")
|
@DELETE("api/v1/services/{id}")
|
||||||
Call<Void> deleteService(@Path("id") Long id);
|
Call<Void> deleteService(@Path("id") Long id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,27 +12,28 @@ import retrofit2.http.PUT;
|
|||||||
import retrofit2.http.Path;
|
import retrofit2.http.Path;
|
||||||
import retrofit2.http.Query;
|
import retrofit2.http.Query;
|
||||||
|
|
||||||
|
//api calls to CRUD suppliers
|
||||||
public interface SupplierApi {
|
public interface SupplierApi {
|
||||||
// Get all suppliers
|
// Get all suppliers
|
||||||
@GET("v1/suppliers")
|
@GET("api/v1/suppliers")
|
||||||
Call<PageResponse<SupplierDTO>> getAllSuppliers(
|
Call<PageResponse<SupplierDTO>> getAllSuppliers(
|
||||||
@Query("page") int page,
|
@Query("page") int page,
|
||||||
@Query("size") int size
|
@Query("size") int size
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get supplier by id
|
// Get supplier by id
|
||||||
@GET("v1/suppliers/{id}")
|
@GET("api/v1/suppliers/{id}")
|
||||||
Call<SupplierDTO> getSupplierById(@Path("id") Long id);
|
Call<SupplierDTO> getSupplierById(@Path("id") Long id);
|
||||||
|
|
||||||
// Create supplier
|
// Create supplier
|
||||||
@POST("v1/suppliers")
|
@POST("api/v1/suppliers")
|
||||||
Call<SupplierDTO> createSupplier(@Body SupplierDTO supplier);
|
Call<SupplierDTO> createSupplier(@Body SupplierDTO supplier);
|
||||||
|
|
||||||
// Update supplier
|
// Update supplier
|
||||||
@PUT("v1/suppliers/{id}")
|
@PUT("api/v1/suppliers/{id}")
|
||||||
Call<SupplierDTO> updateSupplier(@Path("id") Long id, @Body SupplierDTO supplier);
|
Call<SupplierDTO> updateSupplier(@Path("id") Long id, @Body SupplierDTO supplier);
|
||||||
|
|
||||||
// Delete supplier
|
// Delete supplier
|
||||||
@DELETE("v1/suppliers/{id}")
|
@DELETE("api/v1/suppliers/{id}")
|
||||||
Call<Void> deleteSupplier(@Path("id") Long id);
|
Call<Void> deleteSupplier(@Path("id") Long id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package com.example.petstoremobile.api.Auth;
|
package com.example.petstoremobile.api.auth;
|
||||||
|
|
||||||
import com.example.petstoremobile.dtos.AuthDTO;
|
import com.example.petstoremobile.dtos.AuthDTO;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
|
import retrofit2.http.GET;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
|
|
||||||
//Api for logging in
|
//Api for logging in and getting current user
|
||||||
public interface AuthApi {
|
public interface AuthApi {
|
||||||
@POST("v1/auth/login")
|
@POST("api/v1/auth/login")
|
||||||
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
||||||
|
|
||||||
|
@GET("api/v1/auth/me")
|
||||||
|
Call<AuthDTO.UserResponse> getCurrentUser();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.petstoremobile.api.Auth;
|
package com.example.petstoremobile.api.auth;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.petstoremobile.api.Auth;
|
package com.example.petstoremobile.api.auth;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@@ -9,6 +9,7 @@ public class TokenManager {
|
|||||||
private static final String USERNAME_KEY = "username";
|
private static final String USERNAME_KEY = "username";
|
||||||
private static final String ROLE_KEY = "role";
|
private static final String ROLE_KEY = "role";
|
||||||
private static final String PREFS_NAME = "auth_prefs";
|
private static final String PREFS_NAME = "auth_prefs";
|
||||||
|
private static final String USER_ID_KEY = "user_id";
|
||||||
|
|
||||||
private static TokenManager instance;
|
private static TokenManager instance;
|
||||||
private SharedPreferences prefs;
|
private SharedPreferences prefs;
|
||||||
@@ -46,6 +47,15 @@ public class TokenManager {
|
|||||||
return prefs.getString(ROLE_KEY, null);
|
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
|
//Check if logged in
|
||||||
public boolean isLoggedIn() {
|
public boolean isLoggedIn() {
|
||||||
return getToken() != null;
|
return getToken() != null;
|
||||||
@@ -25,4 +25,25 @@ public class AuthDTO {
|
|||||||
public String getUsername() { return username; }
|
public String getUsername() { return username; }
|
||||||
public String getRole() { return role; }
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,44 @@
|
|||||||
package com.example.petstoremobile.dtos;
|
package com.example.petstoremobile.dtos;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
public class MessageDTO {
|
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 content;
|
||||||
private String senderId;
|
|
||||||
private long timestamp;
|
@SerializedName("timestamp")
|
||||||
|
private String timestamp;
|
||||||
|
|
||||||
|
@SerializedName("isRead")
|
||||||
|
private Boolean isRead;
|
||||||
|
|
||||||
public MessageDTO() {}
|
public MessageDTO() {}
|
||||||
|
|
||||||
public MessageDTO(String content) {
|
public Long getId() { return id; }
|
||||||
this.content = content;
|
public void setId(Long id) { this.id = id; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() {
|
public Long getConversationId() { return conversationId; }
|
||||||
return id;
|
public void setConversationId(Long conversationId) { this.conversationId = conversationId; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(String id) {
|
public Long getSenderId() { return senderId; }
|
||||||
this.id = id;
|
public void setSenderId(Long senderId) { this.senderId = senderId; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getContent() {
|
public String getContent() { return content; }
|
||||||
return content;
|
public void setContent(String content) { this.content = content; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setContent(String content) {
|
public String getTimestamp() { return timestamp; }
|
||||||
this.content = content;
|
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getSenderId() {
|
public Boolean getIsRead() { return isRead; }
|
||||||
return senderId;
|
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setSenderId(String senderId) {
|
|
||||||
this.senderId = senderId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(long timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -1,157 +1,407 @@
|
|||||||
package com.example.petstoremobile.fragments;
|
package com.example.petstoremobile.fragments;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.*;
|
||||||
|
import android.widget.*;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
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.R;
|
||||||
import com.example.petstoremobile.adapters.ChatAdapter;
|
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.ChatApi;
|
||||||
import com.example.petstoremobile.api.CustomerApi;
|
import com.example.petstoremobile.api.CustomerApi;
|
||||||
|
import com.example.petstoremobile.api.MessageApi;
|
||||||
import com.example.petstoremobile.api.RetrofitClient;
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
import com.example.petstoremobile.dtos.ConversationDTO;
|
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||||
|
import com.example.petstoremobile.dtos.MessageDTO;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
|
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||||
import com.example.petstoremobile.models.Chat;
|
import com.example.petstoremobile.models.Chat;
|
||||||
|
import com.example.petstoremobile.models.Message;
|
||||||
import java.util.ArrayList;
|
import com.example.petstoremobile.websocket.StompChatManager;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import retrofit2.*;
|
||||||
|
|
||||||
import retrofit2.Call;
|
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
|
||||||
import retrofit2.Callback;
|
StompChatManager.ConversationListener, StompChatManager.ConnectionListener {
|
||||||
import retrofit2.Response;
|
|
||||||
|
|
||||||
|
private static final String TAG = "ChatFragment";
|
||||||
|
|
||||||
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener {
|
// View
|
||||||
|
|
||||||
private LinearLayout chatContainer;
|
|
||||||
private EditText etMessage;
|
|
||||||
private ScrollView scrollView;
|
|
||||||
private Button btnSend;
|
|
||||||
private ImageButton hamburger;
|
|
||||||
private DrawerLayout drawerLayout;
|
private DrawerLayout drawerLayout;
|
||||||
private RecyclerView rvChatList;
|
private RecyclerView rvChatList, rvMessages;
|
||||||
|
private EditText etMessage;
|
||||||
|
private Button btnSend;
|
||||||
|
|
||||||
|
// Adapters
|
||||||
private ChatAdapter chatAdapter;
|
private ChatAdapter chatAdapter;
|
||||||
private List<Chat> chatList = new ArrayList<>();
|
private MessageAdapter messageAdapter;
|
||||||
|
|
||||||
|
// Data
|
||||||
|
private final List<Chat> chatList = new ArrayList<>();
|
||||||
|
private final List<Message> messageList = new ArrayList<>();
|
||||||
|
private final Map<Long, String> customerNames = new HashMap<>();
|
||||||
|
|
||||||
|
// APIs
|
||||||
private ChatApi chatApi;
|
private ChatApi chatApi;
|
||||||
private CustomerApi customerApi;
|
private CustomerApi customerApi;
|
||||||
private Map<Long, String> customerNames = new HashMap<>();
|
private MessageApi messageApi;
|
||||||
|
|
||||||
|
// chat
|
||||||
|
private Long currentUserId;
|
||||||
|
private Long activeConversationId;
|
||||||
|
private StompChatManager stompChatManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
Bundle savedInstanceState) {
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
|
||||||
View view = inflater.inflate(R.layout.fragment_chat, container, false);
|
View view = inflater.inflate(R.layout.fragment_chat, container, false);
|
||||||
|
|
||||||
chatApi = RetrofitClient.getChatApi(requireContext());
|
chatApi = RetrofitClient.getChatApi(requireContext());
|
||||||
customerApi = RetrofitClient.getCustomerApi(requireContext());
|
customerApi = RetrofitClient.getCustomerApi(requireContext());
|
||||||
|
messageApi = RetrofitClient.getMessageApi(requireContext());
|
||||||
|
|
||||||
drawerLayout = view.findViewById(R.id.chatDrawerLayout);
|
drawerLayout = view.findViewById(R.id.chatDrawerLayout);
|
||||||
hamburger = view.findViewById(R.id.btnHamburger);
|
|
||||||
rvChatList = view.findViewById(R.id.rvChatList);
|
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();
|
loadInitialData();
|
||||||
|
|
||||||
// Open the local chat drawer when hamburger is clicked
|
|
||||||
hamburger.setOnClickListener(v -> {
|
|
||||||
if (drawerLayout != null) {
|
|
||||||
drawerLayout.openDrawer(GravityCompat.START);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerViews() {
|
||||||
|
// Set up Drawer menu to select conversation
|
||||||
chatAdapter = new ChatAdapter(chatList, this);
|
chatAdapter = new ChatAdapter(chatList, this);
|
||||||
rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
rvChatList.setAdapter(chatAdapter);
|
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() {
|
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<PageResponse<CustomerDTO>>() {
|
customerApi.getAllCustomers(0, 100).enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<PageResponse<CustomerDTO>> call, Response<PageResponse<CustomerDTO>> response) {
|
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call,
|
||||||
|
@NonNull Response<PageResponse<CustomerDTO>> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
for (CustomerDTO customer : response.body().getContent()) {
|
for (CustomerDTO c : response.body().getContent()) {
|
||||||
customerNames.put(customer.getCustomerId(), customer.getFullName());
|
customerNames.put(c.getCustomerId(), c.getFullName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Then load conversations
|
|
||||||
loadConversations();
|
loadConversations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<PageResponse<CustomerDTO>> call, Throwable t) {
|
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call,
|
||||||
Log.e("ChatFragment", "Error loading customers", t);
|
@NonNull Throwable t) {
|
||||||
loadConversations(); // Try loading conversations anyway
|
loadConversations();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//helper function to load conversations entities to display with customer names in drawer menu
|
||||||
private void loadConversations() {
|
private void loadConversations() {
|
||||||
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<List<ConversationDTO>> call, Response<List<ConversationDTO>> response) {
|
public void onResponse(@NonNull Call<List<ConversationDTO>> call,
|
||||||
|
@NonNull Response<List<ConversationDTO>> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
chatList.clear();
|
chatList.clear();
|
||||||
List<Chat> loadedChats = response.body().stream()
|
List<Chat> loaded = response.body().stream()
|
||||||
.map(dto -> {
|
.map(dto -> {
|
||||||
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
String name = customerNames.getOrDefault(
|
||||||
return new Chat(
|
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||||
String.valueOf(dto.getId()),
|
return new Chat(String.valueOf(dto.getId()),
|
||||||
name,
|
name, dto.getLastMessage(),
|
||||||
dto.getLastMessage());
|
dto.getCustomerId(), dto.getStaffId());
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
chatList.addAll(loadedChats);
|
chatList.addAll(loaded);
|
||||||
chatAdapter.notifyDataSetChanged();
|
chatAdapter.notifyDataSetChanged();
|
||||||
} else {
|
if (activeConversationId == null) {
|
||||||
Log.e("ChatFragment", "Failed to load conversations: " + response.message());
|
messageList.clear();
|
||||||
|
messageAdapter.notifyDataSetChanged();
|
||||||
|
setConversationActive(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<List<ConversationDTO>> call, Throwable t) {
|
public void onFailure(@NonNull Call<List<ConversationDTO>> call,
|
||||||
Log.e("ChatFragment", "Error loading conversations", t);
|
@NonNull Throwable t) {
|
||||||
Toast.makeText(getContext(), "Error loading chats", Toast.LENGTH_SHORT).show();
|
Log.e(TAG, "Error loading conversations", t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when user taps a chat in the drawer
|
||||||
|
// Loads messages for that chat selected
|
||||||
@Override
|
@Override
|
||||||
public void onChatClick(Chat chat) {
|
public void onChatClick(Chat chat) {
|
||||||
// Handle chat selection
|
activeConversationId = Long.parseLong(chat.getChatId());
|
||||||
Toast.makeText(getContext(), "Selected chat: " + chat.getCustomerName(), Toast.LENGTH_SHORT).show();
|
setConversationActive(true);
|
||||||
|
|
||||||
// Close drawer after selection
|
|
||||||
if (drawerLayout != null) {
|
|
||||||
drawerLayout.closeDrawer(GravityCompat.START);
|
drawerLayout.closeDrawer(GravityCompat.START);
|
||||||
|
|
||||||
|
if (stompChatManager != null) {
|
||||||
|
stompChatManager.subscribeToConversation(activeConversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Load actual messages for the selected chat
|
loadMessageHistory(activeConversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//helper function to load messages for selected chat
|
||||||
|
private void loadMessageHistory(Long conversationId) {
|
||||||
|
messageApi.getMessages(conversationId).enqueue(new Callback<List<MessageDTO>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<List<MessageDTO>> call,
|
||||||
|
@NonNull Response<List<MessageDTO>> 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<List<MessageDTO>> 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<MessageDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<MessageDTO> call,
|
||||||
|
@NonNull Response<MessageDTO> 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<MessageDTO> 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.activities.MainActivity;
|
import com.example.petstoremobile.activities.MainActivity;
|
||||||
import com.example.petstoremobile.api.Auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@@ -150,6 +150,7 @@ public class ProfileFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.show();
|
.show();
|
||||||
|
//TODO: UPDATE PHOTO IN DATABASE
|
||||||
});
|
});
|
||||||
|
|
||||||
//Edit email button
|
//Edit email button
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ public class Chat {
|
|||||||
private String chatId;
|
private String chatId;
|
||||||
private String customerName;
|
private String customerName;
|
||||||
private String lastMessage;
|
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.chatId = chatId;
|
||||||
this.customerName = customerName;
|
this.customerName = customerName;
|
||||||
this.lastMessage = lastMessage;
|
this.lastMessage = lastMessage;
|
||||||
|
this.customerId = customerId;
|
||||||
|
this.staffId = staffId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getChatId() {
|
public String getChatId() {
|
||||||
@@ -22,4 +26,12 @@ public class Chat {
|
|||||||
public String getLastMessage() {
|
public String getLastMessage() {
|
||||||
return lastMessage;
|
return lastMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getStaffId() {
|
||||||
|
return staffId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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<String, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/src/main/res/drawable/bg_message_received.xml
Normal file
9
app/src/main/res/drawable/bg_message_received.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#E0E0E0" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="16dp"
|
||||||
|
android:topRightRadius="16dp"
|
||||||
|
android:bottomRightRadius="16dp"
|
||||||
|
android:bottomLeftRadius="2dp" />
|
||||||
|
</shape>
|
||||||
9
app/src/main/res/drawable/bg_message_sent.xml
Normal file
9
app/src/main/res/drawable/bg_message_sent.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#E74C3C" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="16dp"
|
||||||
|
android:topRightRadius="16dp"
|
||||||
|
android:bottomRightRadius="2dp"
|
||||||
|
android:bottomLeftRadius="16dp" />
|
||||||
|
</shape>
|
||||||
@@ -37,21 +37,13 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ScrollView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/scrollView"
|
android:id="@+id/rvMessages"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:padding="8dp">
|
android:padding="8dp"
|
||||||
|
android:clipToPadding="false" />
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/chatContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="8dp"/>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
19
app/src/main/res/layout/item_message_received.xml
Normal file
19
app/src/main/res/layout/item_message_received.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:gravity="start">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMessageContent"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_message_received"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="Received message"
|
||||||
|
android:textColor="@color/text_dark"
|
||||||
|
android:maxWidth="300dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
19
app/src/main/res/layout/item_message_sent.xml
Normal file
19
app/src/main/res/layout/item_message_sent.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMessageContent"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_message_sent"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="Sent message"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:maxWidth="300dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -4,8 +4,28 @@
|
|||||||
<item name="colorPrimary">@color/primary_dark</item>
|
<item name="colorPrimary">@color/primary_dark</item>
|
||||||
<item name="colorPrimaryVariant">@color/primary_medium</item>
|
<item name="colorPrimaryVariant">@color/primary_medium</item>
|
||||||
<item name="colorAccent">@color/accent_coral</item>
|
<item name="colorAccent">@color/accent_coral</item>
|
||||||
<item name="android:windowBackground">@color/background_grey</item>
|
<item name="android:windowBackground">@color/primary_dark</item>
|
||||||
<item name="editTextStyle">@style/Widget.App.EditText</item>
|
<item name="editTextStyle">@style/Widget.App.EditText</item>
|
||||||
<item name="spinnerStyle">@style/Widget.App.Spinner</item>
|
<item name="spinnerStyle">@style/Widget.App.Spinner</item>
|
||||||
|
<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog</item>
|
||||||
|
<item name="alertDialogTheme">@style/Theme.App.AlertDialog</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="ThemeOverlay.App.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
||||||
|
<item name="colorPrimary">@color/white</item>
|
||||||
|
<item name="colorOnSurface">@color/white</item>
|
||||||
|
<item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button.TextButton</item>
|
||||||
|
<item name="buttonBarNegativeButtonStyle">@style/Widget.App.Button.TextButton</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.App.AlertDialog" parent="Theme.AppCompat.Dialog.Alert">
|
||||||
|
<item name="colorAccent">@color/white</item>
|
||||||
|
<item name="android:textColorPrimary">@color/white</item>
|
||||||
|
<item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button.TextButton</item>
|
||||||
|
<item name="buttonBarNegativeButtonStyle">@style/Widget.App.Button.TextButton</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.App.Button.TextButton" parent="Widget.Material3.Button.TextButton">
|
||||||
|
<item name="android:textColor">@color/white</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -19,10 +19,10 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "PetStoreMobile"
|
rootProject.name = "PetStoreMobile"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
||||||
Reference in New Issue
Block a user