diff --git a/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java b/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java index 4f8f14b..3c1cf50 100644 --- a/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java +++ b/app/src/main/java/com/example/tampopo_client/models/FriendRequest.java @@ -4,6 +4,8 @@ private Integer id; private String senderId; private String receiverId; + private String receiverName; + private String receiverIcon; public FriendRequest(){ //デフォルトコンストラクタを追加しました @@ -36,4 +38,20 @@ public void setReceiverId(String receiverId) { this.receiverId = receiverId; } + + public String getReceiverName() { + return receiverName; + } + + public void setReceiverName(String receiverName) { + this.receiverName = receiverName; + } + + public String getReceiverIcon() { + return receiverIcon; + } + + public void setReceiverIcon(String receiverIcon) { + this.receiverIcon = receiverIcon; + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java b/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java index b82d1ae..bc52a64 100644 --- a/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java +++ b/app/src/main/java/com/example/tampopo_client/resources/FriendRequestsResource.java @@ -33,6 +33,6 @@ @DELETE("friend-requests/{friend-request-id}") Call deleteFriendRequest( @Path("friend-request-id") String friendRequestId, - @Query("token") String token + @Query("receiver-token") String token ); } \ No newline at end of file diff --git a/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java b/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java index 3cc0a89..954c9c1 100644 --- a/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java +++ b/app/src/main/java/com/example/tampopo_client/viewmodels/FriendSentRequestViewModel.java @@ -5,7 +5,9 @@ import com.example.tampopo_client.models.FriendRequest; import com.example.tampopo_client.resources.FriendRequestsResource; +import com.example.tampopo_client.resources.UserResource; +import java.util.ArrayList; import java.util.List; import retrofit2.Call; @@ -21,6 +23,7 @@ // APIの窓口 private final FriendRequestsResource friendRequestsResource; + private final UserResource userResource; // 自分が送ったフレンド申請一覧 private final MutableLiveData> sentRequests; @@ -28,6 +31,10 @@ // 処理の進行状況や結果を表示するためのステータスメッセージ private final MutableLiveData operationResult; + // 直近で使用したユーザーIDをキャッシュしておく + private String cachedUserId; + private String cachedToken; + // コンストラクタ(ViewModel生成時に呼び出される) public FriendSentRequestViewModel() { this.retrofit = new Retrofit.Builder() @@ -37,6 +44,7 @@ .build(); this.friendRequestsResource = retrofit.create(FriendRequestsResource.class); + this.userResource = retrofit.create(UserResource.class); this.sentRequests = new MutableLiveData<>(); this.operationResult = new MutableLiveData<>(); } @@ -51,16 +59,33 @@ } // サーバーから送信済みフレンドリクエスト一覧を取得してキャッシュに保存 - public void loadSentRequests(String token) { + public void loadSentRequests(String token, String myUserId) { + // キャッシュ保存(削除後の再読み込み用) + this.cachedUserId = myUserId; + this.cachedToken = token; + Call> call = friendRequestsResource.getFriendRequests(token); // フレンド一覧をサーバーからもらう時にエラーが起きている call.enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { if (response.isSuccessful()) { - sentRequests.setValue(response.body()); - operationResult.setValue("Success"); - System.out.println("Success SetValue: " + response.body()); + List allRequests = response.body(); + if (allRequests == null) allRequests = new ArrayList<>(); + + // 自分が送ったものだけを抽出 + List sentOnly = new ArrayList<>(); + for (FriendRequest req : allRequests) { + if (req.getSenderId() != null && req.getSenderId().equals(myUserId)) { + sentOnly.add(req); + } + } + + // 各リクエストに対してユーザー情報を取得 + fetchUserInfoForRequests(sentOnly); + + operationResult.setValue("Success (filtered " + sentOnly.size() + " requests)"); + System.out.println("Success SetValue (filtered): " + sentOnly); } else { operationResult.setValue("Error: " + response.code()); System.out.println("Response error: " + response.code()); @@ -87,7 +112,7 @@ operationResult.setValue("Friend request sent successfully."); // 成功時に送信済みリストを更新 - loadSentRequests(token); + loadSentRequests(token, senderId); System.out.println("Friend request sent: " + response.body()); } else { @@ -103,6 +128,108 @@ } }); } + + // 各FriendRequestに対してユーザー情報を取得するメソッド + private void fetchUserInfoForRequests(List requests) { + if (requests == null || requests.isEmpty()) { + sentRequests.setValue(requests); + return; + } + + int totalRequests = requests.size(); + final int[] completedRequests = {0}; + + for (FriendRequest req : requests) { + String receiverId = req.getReceiverId(); + if (receiverId == null) { + completedRequests[0]++; + if (completedRequests[0] == totalRequests) { + sentRequests.setValue(requests); + } + continue; + } + + // ニックネームを取得 + Call nameCall = userResource.getName(receiverId); + nameCall.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + req.setReceiverName(response.body()); + } + checkAndUpdateLiveData(); + } + + @Override + public void onFailure(Call call, Throwable t) { + System.out.println("Failed to fetch name for " + receiverId + ": " + t.getMessage()); + checkAndUpdateLiveData(); + } + + private void checkAndUpdateLiveData() { + completedRequests[0]++; + if (completedRequests[0] == totalRequests * 2) { + sentRequests.setValue(requests); + } + } + }); + + // アイコンを取得 + Call iconCall = userResource.getIcon(receiverId); + iconCall.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + req.setReceiverIcon(response.body()); + } + checkAndUpdateLiveData(); + } + + @Override + public void onFailure(Call call, Throwable t) { + System.out.println("Failed to fetch icon for " + receiverId + ": " + t.getMessage()); + checkAndUpdateLiveData(); + } + + private void checkAndUpdateLiveData() { + completedRequests[0]++; + if (completedRequests[0] == totalRequests * 2) { + sentRequests.setValue(requests); + } + } + }); + } + } + + // サーバーからフレンド申請を削除するメソッド + public void deleteFriendRequest(String friendRequestId, String token) { + Call call = friendRequestsResource.deleteFriendRequest(friendRequestId, token); + + call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + operationResult.setValue("Friend request deleted successfully."); + + // 成功時に送信済みリストを更新(キャッシュがある場合) + if (cachedToken != null && cachedUserId != null) { + loadSentRequests(cachedToken, cachedUserId); + } + + System.out.println("Friend request deleted successfully"); + } else { + operationResult.setValue("Error deleting request: " + response.code()); + System.out.println("Error deleting request: " + response.code()); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + operationResult.setValue("Network error: " + t.getMessage()); + System.out.println("Network error: " + t); + } + }); + } } diff --git a/app/src/main/java/com/example/tampopo_client/views/FriendActivity.java b/app/src/main/java/com/example/tampopo_client/views/FriendActivity.java index f556cc1..a5e05bf 100644 --- a/app/src/main/java/com/example/tampopo_client/views/FriendActivity.java +++ b/app/src/main/java/com/example/tampopo_client/views/FriendActivity.java @@ -47,6 +47,8 @@ if (position == 0) { tab.setText("フレンド"); } else if (position == 1) { + tab.setText("申請中"); + } else if (position == 2) { tab.setText("保留中"); } }).attach(); diff --git a/app/src/main/java/com/example/tampopo_client/views/FriendSentFragment.java b/app/src/main/java/com/example/tampopo_client/views/FriendSentFragment.java new file mode 100644 index 0000000..14b425d --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/views/FriendSentFragment.java @@ -0,0 +1,114 @@ +package com.example.tampopo_client.views; + +import android.content.Context; +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.example.tampopo_client.R; +import com.example.tampopo_client.Tampopo; +import com.example.tampopo_client.models.FriendRequest; +import com.example.tampopo_client.viewmodels.FriendSentRequestViewModel; + +import java.util.ArrayList; +import java.util.List; + +/** + * 友達申請(送信リスト)を表示するための Fragment クラス + */ +public class FriendSentFragment extends Fragment { + + // RecyclerView を縦に並べるか、グリッドで並べるかを制御するためのカラム数 + private static final String ARG_COLUMN_COUNT = "column-count"; + // TODO: Customize parameters + private int mColumnCount = 1; // デフォルトは1列(縦方向リスト) + + // アプリ全体で共有するクラス(ユーザー情報やトークンを持っている) + private Tampopo tampopo; + // RecyclerView のアダプタ(友達申請を表示する) + private MyFriendSentRequestRecyclerViewAdapter adapter; + + // デフォルトのコンストラクタ(Fragmentは必須) + public FriendSentFragment() { + } + + // この Fragment をインスタンス化するときに、カラム数を指定するためのファクトリーメソッド + @SuppressWarnings("unused") + public static FriendSentFragment newInstance(int columnCount) { + FriendSentFragment fragment = new FriendSentFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_COLUMN_COUNT, columnCount); // 引数に列数を渡す + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // newInstance で渡された引数を取り出す + if (getArguments() != null) { + mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT); + } + } + + //Fragment のレイアウトを作成する部分 + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // レイアウト XML を読み込む + View view = inflater.inflate(R.layout.fragment_friend_sent_list, container, false); + + // Application クラスからユーザー情報を取得 + tampopo = (Tampopo) getActivity().getApplication(); + String senderId = tampopo.getUserId(); // 自分のユーザーID + String token = tampopo.getToken(); // 認証トークン + + // RecyclerView の初期化 + if (view instanceof RecyclerView) { + Context context = view.getContext(); + RecyclerView recyclerView = (RecyclerView) view; + // 1列なら縦スクロールリスト、2列以上ならグリッド表示 + if (mColumnCount <= 1) { + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + } else { + recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); + } + // アダプタを生成してセット(最初は空のリストを渡す) + adapter = new MyFriendSentRequestRecyclerViewAdapter(new ArrayList<>(), senderId, token, getContext()); + recyclerView.setAdapter(adapter); + } + return view; + } + + //View が作成されたあとに呼ばれる。ここで LiveData の監視を始める。 + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // 送信した友達申請を管理する ViewModel を取得 + FriendSentRequestViewModel friendSentRequestViewModel = new ViewModelProvider(this).get(FriendSentRequestViewModel.class); + // サーバーから送信した友達リクエスト一覧をロード + friendSentRequestViewModel.loadSentRequests(tampopo.getToken(), tampopo.getUserId()); + // LiveData を監視して、データが変わったら RecyclerView に反映 + friendSentRequestViewModel.getSentRequestsLiveData().observe(getViewLifecycleOwner(), new Observer>() { + + // LiveData に変更があったとき(新しい友達リクエストのリストが届いたとき)に呼ばれるメソッド + @Override + public void onChanged(List friendRequests) { + if(adapter != null) { + adapter.setItems(friendRequests); + adapter.notifyDataSetChanged(); + } + } + }); + } +} diff --git a/app/src/main/java/com/example/tampopo_client/views/MyFragmentAdapter.java b/app/src/main/java/com/example/tampopo_client/views/MyFragmentAdapter.java index bdde5e5..42dc26d 100644 --- a/app/src/main/java/com/example/tampopo_client/views/MyFragmentAdapter.java +++ b/app/src/main/java/com/example/tampopo_client/views/MyFragmentAdapter.java @@ -15,17 +15,20 @@ @Override public Fragment createFragment(int position) { if (position == 0) { - // フラグメント①:申請を受けた + // フラグメント①:友達リスト return new FriendListFragment(); + } else if (position == 1) { + // フラグメント②:申請中 + return new FriendSentFragment(); } else { - // フラグメント②:友達リスト + // フラグメント③:保留中(申請を受けた) return new FriendReceivedFragment(); } } @Override public int getItemCount() { - return 2; + return 3; } } diff --git a/app/src/main/java/com/example/tampopo_client/views/MyFriendSentRequestRecyclerViewAdapter.java b/app/src/main/java/com/example/tampopo_client/views/MyFriendSentRequestRecyclerViewAdapter.java new file mode 100644 index 0000000..48b75f6 --- /dev/null +++ b/app/src/main/java/com/example/tampopo_client/views/MyFriendSentRequestRecyclerViewAdapter.java @@ -0,0 +1,158 @@ +package com.example.tampopo_client.views; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; + +import android.app.Dialog; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.bumptech.glide.Glide; + +import com.example.tampopo_client.R; +import com.example.tampopo_client.models.FriendRequest; +import com.example.tampopo_client.viewmodels.FriendSentRequestViewModel; +import com.example.tampopo_client.databinding.FragmentFriendSentBinding; + +import java.util.ArrayList; +import java.util.List; + + +// このクラスは RecyclerView.Adapter を継承していて、送信済みFriendRequest を表示するアダプター +public class MyFriendSentRequestRecyclerViewAdapter extends RecyclerView.Adapter { + + // 表示するFriendRequestのリスト(アダプターのデータ) + private final List items; + + private String senderId; + private String token; + private Context context; + + // コンストラクタ:アダプターを初期化し、表示データのリストを受け取る + public MyFriendSentRequestRecyclerViewAdapter(List items, String senderId, String token, Context context) { + this.items = new ArrayList<>(items); + this.senderId = senderId; + this.token = token; + this.context = context; + } + + public void setItems(List newItems) { + items.clear(); + items.addAll(newItems); + } + + // ビュー(行)を新しく作成するときに呼ばれる(レイアウトのXMLを元に1行分のViewを作成) + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + // fragment_friend_sent.xml を元に View を生成して ViewHolder に渡す + return new ViewHolder(FragmentFriendSentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + // 表示するデータを、対応するViewHolderのUI部品にセットする(スクロール時など) + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + // 現在の位置の FriendRequest を取得して ViewHolder に保持させる + holder.mItem = items.get(position); + FriendRequest request = items.get(position); + + // ニックネームを表示(存在すれば) + if (request.getReceiverName() != null && !request.getReceiverName().isEmpty()) { + holder.mContentView.setText(request.getReceiverName()); + } else { + holder.mContentView.setText("名前未設定"); + } + + // ユーザーIDを表示 + holder.mUserIdView.setText(request.getReceiverId()); + + // アイコンを表示(Glideを使用) + if (request.getReceiverIcon() != null && !request.getReceiverIcon().isEmpty()) { + Glide.with(holder.mImageView.getContext()) + .load(request.getReceiverIcon()) + .placeholder(R.drawable.friend) + .error(R.drawable.friend) + .into(holder.mImageView); + } else { + holder.mImageView.setImageResource(R.drawable.friend); + } + + // 「申請中」ボタンを押した時の処理 + holder.pendingButton.setOnClickListener(v -> { + // 削除確認ダイアログを表示 + showDeleteConfirmDialog(v.getContext(), holder.mItem); + }); + } + + // リスト全体のアイテム数を返す(RecyclerViewに何個表示するかを教える) + @Override + public int getItemCount() { + return items.size(); + } + + // 削除確認ダイアログを表示するメソッド + private void showDeleteConfirmDialog(Context context, FriendRequest friendRequest) { + Dialog dialog = new Dialog(context); + dialog.setContentView(R.layout.dialog_delete_friend_request); + dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); + dialog.getWindow().setDimAmount(0.5f); + + // メッセージテキスト + TextView tvMessage = dialog.findViewById(R.id.tv_message); + String displayName = friendRequest.getReceiverName() != null && !friendRequest.getReceiverName().isEmpty() + ? friendRequest.getReceiverName() + : friendRequest.getReceiverId(); + tvMessage.setText(displayName + " さんへの申請を取りやめますか?"); + + // 削除ボタン + Button btnDelete = dialog.findViewById(R.id.btn_delete); + btnDelete.setOnClickListener(v -> { + // ViewModelを取得してフレンド申請を削除 + FriendSentRequestViewModel viewModel = new ViewModelProvider((ViewModelStoreOwner) context).get(FriendSentRequestViewModel.class); + viewModel.deleteFriendRequest(String.valueOf(friendRequest.getId()), token); + + Toast.makeText(context, "申請を取りやめました", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + // キャンセルボタン + Button btnCancel = dialog.findViewById(R.id.btn_cancel); + btnCancel.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); + } + + // 各行(View)を保持するための ViewHolder クラス + public class ViewHolder extends RecyclerView.ViewHolder { + // TextView:表示される友達の名前 + public final TextView mContentView; + public final TextView mUserIdView; + public final ImageView mImageView; + // 表示する1つのFriendRequestを保持 + public FriendRequest mItem; + public View pendingButton; + + // ViewHolderのコンストラクタ:バインディングされたViewを使ってUI部品にアクセスする + public ViewHolder(FragmentFriendSentBinding binding) { + // 親クラスのコンストラクタに、View全体を渡す + super(binding.getRoot()); + // fragment_friend_sent.xml 内の TextView(contentというid)に対応する変数をセット + mContentView = binding.content; + mUserIdView = binding.userId; + mImageView = binding.imageView2; + pendingButton = binding.PendingButton; + } + + // デバッグやログ出力用に、表示中のテキストを返す + @Override + public String toString() { + return super.toString() + " '" + mContentView.getText() + "'"; + } + } +} diff --git a/app/src/main/res/layout/dialog_delete_friend_request.xml b/app/src/main/res/layout/dialog_delete_friend_request.xml new file mode 100644 index 0000000..02c56d2 --- /dev/null +++ b/app/src/main/res/layout/dialog_delete_friend_request.xml @@ -0,0 +1,55 @@ + + + + + + + + + + +