diff --git a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt index 5895d71..9771a1f 100644 --- a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt +++ b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt @@ -45,7 +45,12 @@ interface TecnologiaApi { @Part file: MultipartBody.Part ): Response - // contact + + + // friends @GET("api/friends") suspend fun getFriends(): FriendListResponse + + @GET("api/friends/pending") + suspend fun getFriendsPending(): PendingFriendsResponse } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/data/api/api_class/FriendRequest.kt b/app/src/main/java/com/example/login_v3/data/api/api_class/FriendRequest.kt index 7ed97e5..53a72f9 100644 --- a/app/src/main/java/com/example/login_v3/data/api/api_class/FriendRequest.kt +++ b/app/src/main/java/com/example/login_v3/data/api/api_class/FriendRequest.kt @@ -10,23 +10,22 @@ data class FriendListResponse( ) @JsonClass(generateAdapter = true) data class Friend( - @Json(name = "friend_id") + @field:Json(name = "friend_id") val friendId: String, val username: String, - @Json(name = "display_name") + @field:Json(name = "display_name") val displayName: String, - @Json(name = "avatar_url") + @field:Json(name = "avatar_url") val avatarUrl: String?, // 建議給可空性,避免後端沒傳頭像時崩潰 val status: String, - @Json(name = "accepted_at") + @field:Json(name = "accepted_at") val acceptedAt: String ) - val Friend.fullFriendsAvatarUrl: Any get() = if (avatarUrl.isNullOrBlank()) { R.drawable.avatar_v1 @@ -34,4 +33,42 @@ val Friend.fullFriendsAvatarUrl: Any avatarUrl } else { "http://192.168.0.217$avatarUrl" - } \ No newline at end of file + } + + + +//friends pending request +@JsonClass(generateAdapter = true) +data class PendingFriendsResponse( + @field:Json(name = "pending") + val pendingRequests: List +) + +@JsonClass(generateAdapter = true) +data class PendingFriendApiModel( + @field:Json(name = "friendship_id") + val friendshipId: String, + + val username: String, + + @field:Json(name = "display_name") + val displayName: String, + + @field:Json(name = "avatar_url") + val PendingAvatarUrl: String?, + + @field:Json(name = "from_user_id") + val fromUserId: String, + + @field:Json(name = "created_at") + val createdAt: String +) + +val PendingFriendApiModel.fullPendingAvatarUrl: Any + get() = if (PendingAvatarUrl.isNullOrBlank()) { + R.drawable.avatar_v1 + } else if (PendingAvatarUrl.startsWith("http")) { + PendingAvatarUrl + } else { + "http://192.168.0.217$PendingAvatarUrl" + } diff --git a/app/src/main/java/com/example/login_v3/data/repository/basic/Friends_Repository.kt b/app/src/main/java/com/example/login_v3/data/repository/basic/Friends_Repository.kt index 9795736..2df6723 100644 --- a/app/src/main/java/com/example/login_v3/data/repository/basic/Friends_Repository.kt +++ b/app/src/main/java/com/example/login_v3/data/repository/basic/Friends_Repository.kt @@ -2,12 +2,14 @@ package com.example.login_v3.data.repository.basic import com.example.login_v3.data.api.TecnologiaApi import com.example.login_v3.data.api.api_class.Friend +import com.example.login_v3.data.api.api_class.PendingFriendApiModel import javax.inject.Inject -class FriendsRepository @Inject constructor - (private val apiService: TecnologiaApi) { +class FriendsRepository @Inject constructor( + private val apiService: TecnologiaApi +) { - suspend fun getFriendList(): Result> { + suspend fun getFriendList(): Result> { return try { val response = apiService.getFriends() // 回傳成功,並直接提取裡面的 List @@ -17,4 +19,14 @@ class FriendsRepository @Inject constructor Result.failure(e) } } + + suspend fun getPendingRequests(): Result> { + return try { + val response = apiService.getFriendsPending() + // 這裡回傳 API Model,轉換邏輯可以放在 ViewModel + Result.success(response.pendingRequests) + } catch (e: Exception) { + Result.failure(e) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_contact_list.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_contact_list.kt index 6a5b45e..4864043 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_contact_list.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_contact_list.kt @@ -2,14 +2,22 @@ package com.example.login_v3.home.Message.UI.Detail import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -25,11 +33,14 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil.compose.AsyncImage import com.example.login_v3.R import com.example.login_v3.data.api.api_class.Friend +import com.example.login_v3.data.api.api_class.PendingFriendApiModel import com.example.login_v3.data.api.api_class.fullFriendsAvatarUrl +import com.example.login_v3.data.api.api_class.fullPendingAvatarUrl import com.example.login_v3.home.Message.ViewModel.Detail.ContactListViewModel import com.example.login_v3.navigation.BottomBarViewModel @@ -39,6 +50,8 @@ fun Message_contact_list( contactListViewModel: ContactListViewModel = hiltViewModel(), bottomBarViewModel: BottomBarViewModel ){ + + val uiState by contactListViewModel.uiState.collectAsStateWithLifecycle() val friends by contactListViewModel.friends.collectAsStateWithLifecycle() DisposableEffect(Unit) { @@ -56,23 +69,72 @@ fun Message_contact_list( ) { Spacer(modifier = Modifier.height(60.dp)) - if (friends.isEmpty()) { - // 如果列表是空的,顯示提示(或載入指示器) - Box( - modifier = Modifier.fillMaxSize().padding(6.dp), - contentAlignment = Alignment.Center - ) { - Text("目前沒有好友") - } - } else { - // 顯示好友列表 - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(6.dp) - ) { - items(friends) { friend -> - FriendRow(friend) + //pending friends list + Column( + modifier = Modifier + .fillMaxWidth() + ) { + if (uiState.pendingRequests.isNotEmpty()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(6.dp) + ) { + item { + Text( + "好友請求 (${uiState.pendingRequests.size})", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + items(uiState.pendingRequests) { request -> + PendingFriendRow(request) + } + item { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) } + } + } + + } + + //friends list + Column( + modifier = Modifier + .fillMaxWidth() + ) { + if (friends.isEmpty()) { + // 如果列表是空的,顯示提示(或載入指示器) + Box( + modifier = Modifier.fillMaxSize().padding(6.dp), + contentAlignment = Alignment.Center + ) { + Text("目前沒有好友") + } + } else { + // 顯示好友列表 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(6.dp) + ) { + item { + Text( + "所有好友", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + + if (uiState.friends.isEmpty() && !uiState.isLoading) { + item { + Box(modifier = Modifier.fillMaxWidth().padding(32.dp), contentAlignment = Alignment.Center) { + Text("目前沒有好友") + } + } + } else { + items(uiState.friends) { friend -> + FriendRow(friend) + } + } } } } @@ -107,4 +169,31 @@ fun FriendRow(friend: Friend) { ) } ) +} + +@Composable +fun PendingFriendRow(request: PendingFriendApiModel) { + ListItem( + headlineContent = { Text(request.displayName) }, + supportingContent = { Text("想加你為好友") }, + leadingContent = { + AsyncImage( + // 這裡可以使用我們之前寫的擴充函式 .toFullImageUrl() + model = request.fullPendingAvatarUrl, + contentDescription = null, + modifier = Modifier.size(40.dp).clip(CircleShape), + error = painterResource(id = R.drawable.avatar_v1) + ) + }, + trailingContent = { + Row { + IconButton(onClick = { /* TODO: 呼叫 ViewModel 拒絕 */ }) { + Icon(Icons.Default.Close, contentDescription = "拒絕", tint = Color.Red) + } + IconButton(onClick = { /* TODO: 呼叫 ViewModel 接受 */ }) { + Icon(Icons.Default.Check, contentDescription = "接受", tint = Color.Green) + } + } + } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Contact_list_ViewModel.kt b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Contact_list_ViewModel.kt index b256abc..49be749 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Contact_list_ViewModel.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Contact_list_ViewModel.kt @@ -3,36 +3,64 @@ package com.example.login_v3.home.Message.ViewModel.Detail import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.login_v3.data.api.api_class.Friend +import com.example.login_v3.data.api.api_class.PendingFriendApiModel import com.example.login_v3.data.repository.basic.FriendsRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject -@HiltViewModel -class ContactListViewModel @Inject constructor - ( - private val repository: FriendsRepository - ) : ViewModel() { +data class ContactUiState( + val isLoading: Boolean = false, + val friends: List = emptyList(), + val pendingRequests: List = emptyList(), + val error: String? = null +) +@HiltViewModel +class ContactListViewModel @Inject constructor( + private val repository: FriendsRepository +) : ViewModel() { + + + private val _uiState = MutableStateFlow(ContactUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private val _friends = MutableStateFlow>(emptyList()) val friends: StateFlow> = _friends init { // 畫面一建立就自動抓取資料 fetchFriends() + fetchPendingRequests() } + + //friends list fun fetchFriends() { viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } repository.getFriendList() .onSuccess { list -> - _friends.value = list + _uiState.update { it.copy(isLoading = false, friends = list) } } - .onFailure { - // 這裡可以處理錯誤訊息,例如顯示 Toast + .onFailure { error -> + _uiState.update { it.copy(isLoading = false, error = error.message) } } } } + + + // 獲取待處理請求 (API: /api/friends/pending) + fun fetchPendingRequests() { + viewModelScope.launch { + repository.getPendingRequests() + .onSuccess { requests -> + _uiState.update { it.copy(pendingRequests = requests) } + } + .onFailure { /* 處理錯誤 */ } + } + } } \ No newline at end of file