auth to server-V66
This commit is contained in:
parent
7f95b6b187
commit
e346444e97
5 changed files with 206 additions and 35 deletions
|
|
@ -45,7 +45,12 @@ interface TecnologiaApi {
|
||||||
@Part file: MultipartBody.Part
|
@Part file: MultipartBody.Part
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
// contact
|
|
||||||
|
|
||||||
|
// friends
|
||||||
@GET("api/friends")
|
@GET("api/friends")
|
||||||
suspend fun getFriends(): FriendListResponse
|
suspend fun getFriends(): FriendListResponse
|
||||||
|
|
||||||
|
@GET("api/friends/pending")
|
||||||
|
suspend fun getFriendsPending(): PendingFriendsResponse
|
||||||
}
|
}
|
||||||
|
|
@ -10,23 +10,22 @@ data class FriendListResponse(
|
||||||
)
|
)
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Friend(
|
data class Friend(
|
||||||
@Json(name = "friend_id")
|
@field:Json(name = "friend_id")
|
||||||
val friendId: String,
|
val friendId: String,
|
||||||
|
|
||||||
val username: String,
|
val username: String,
|
||||||
|
|
||||||
@Json(name = "display_name")
|
@field:Json(name = "display_name")
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
|
|
||||||
@Json(name = "avatar_url")
|
@field:Json(name = "avatar_url")
|
||||||
val avatarUrl: String?, // 建議給可空性,避免後端沒傳頭像時崩潰
|
val avatarUrl: String?, // 建議給可空性,避免後端沒傳頭像時崩潰
|
||||||
|
|
||||||
val status: String,
|
val status: String,
|
||||||
|
|
||||||
@Json(name = "accepted_at")
|
@field:Json(name = "accepted_at")
|
||||||
val acceptedAt: String
|
val acceptedAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
val Friend.fullFriendsAvatarUrl: Any
|
val Friend.fullFriendsAvatarUrl: Any
|
||||||
get() = if (avatarUrl.isNullOrBlank()) {
|
get() = if (avatarUrl.isNullOrBlank()) {
|
||||||
R.drawable.avatar_v1
|
R.drawable.avatar_v1
|
||||||
|
|
@ -35,3 +34,41 @@ val Friend.fullFriendsAvatarUrl: Any
|
||||||
} else {
|
} else {
|
||||||
"http://192.168.0.217$avatarUrl"
|
"http://192.168.0.217$avatarUrl"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//friends pending request
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PendingFriendsResponse(
|
||||||
|
@field:Json(name = "pending")
|
||||||
|
val pendingRequests: List<PendingFriendApiModel>
|
||||||
|
)
|
||||||
|
|
||||||
|
@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"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.TecnologiaApi
|
||||||
import com.example.login_v3.data.api.api_class.Friend
|
import com.example.login_v3.data.api.api_class.Friend
|
||||||
|
import com.example.login_v3.data.api.api_class.PendingFriendApiModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FriendsRepository @Inject constructor
|
class FriendsRepository @Inject constructor(
|
||||||
(private val apiService: TecnologiaApi) {
|
private val apiService: TecnologiaApi
|
||||||
|
) {
|
||||||
|
|
||||||
suspend fun getFriendList(): Result<List<Friend>> {
|
suspend fun getFriendList(): Result<List<Friend>> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.getFriends()
|
val response = apiService.getFriends()
|
||||||
// 回傳成功,並直接提取裡面的 List<Friend>
|
// 回傳成功,並直接提取裡面的 List<Friend>
|
||||||
|
|
@ -17,4 +19,14 @@ class FriendsRepository @Inject constructor
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getPendingRequests(): Result<List<PendingFriendApiModel>> {
|
||||||
|
return try {
|
||||||
|
val response = apiService.getFriendsPending()
|
||||||
|
// 這裡回傳 API Model,轉換邏輯可以放在 ViewModel
|
||||||
|
Result.success(response.pendingRequests)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,14 +2,22 @@ package com.example.login_v3.home.Message.UI.Detail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
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.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -25,11 +33,14 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.example.login_v3.R
|
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.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.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.home.Message.ViewModel.Detail.ContactListViewModel
|
||||||
import com.example.login_v3.navigation.BottomBarViewModel
|
import com.example.login_v3.navigation.BottomBarViewModel
|
||||||
|
|
||||||
|
|
@ -39,6 +50,8 @@ fun Message_contact_list(
|
||||||
contactListViewModel: ContactListViewModel = hiltViewModel(),
|
contactListViewModel: ContactListViewModel = hiltViewModel(),
|
||||||
bottomBarViewModel: BottomBarViewModel
|
bottomBarViewModel: BottomBarViewModel
|
||||||
){
|
){
|
||||||
|
|
||||||
|
val uiState by contactListViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val friends by contactListViewModel.friends.collectAsStateWithLifecycle()
|
val friends by contactListViewModel.friends.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
|
|
@ -56,23 +69,72 @@ fun Message_contact_list(
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(60.dp))
|
Spacer(modifier = Modifier.height(60.dp))
|
||||||
|
|
||||||
if (friends.isEmpty()) {
|
//pending friends list
|
||||||
// 如果列表是空的,顯示提示(或載入指示器)
|
Column(
|
||||||
Box(
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxSize().padding(6.dp),
|
.fillMaxWidth()
|
||||||
contentAlignment = Alignment.Center
|
) {
|
||||||
) {
|
if (uiState.pendingRequests.isNotEmpty()) {
|
||||||
Text("目前沒有好友")
|
LazyColumn(
|
||||||
}
|
modifier = Modifier
|
||||||
} else {
|
.fillMaxSize()
|
||||||
// 顯示好友列表
|
.padding(6.dp)
|
||||||
LazyColumn(
|
) {
|
||||||
modifier = Modifier
|
item {
|
||||||
.fillMaxSize()
|
Text(
|
||||||
.padding(6.dp)
|
"好友請求 (${uiState.pendingRequests.size})",
|
||||||
) {
|
style = MaterialTheme.typography.titleMedium,
|
||||||
items(friends) { friend ->
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
FriendRow(friend)
|
)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,3 +170,30 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,36 +3,64 @@ package com.example.login_v3.home.Message.ViewModel.Detail
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.login_v3.data.api.api_class.Friend
|
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 com.example.login_v3.data.repository.basic.FriendsRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ContactListViewModel @Inject constructor
|
|
||||||
(
|
|
||||||
private val repository: FriendsRepository
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
|
data class ContactUiState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val friends: List<Friend> = emptyList(),
|
||||||
|
val pendingRequests: List<PendingFriendApiModel> = emptyList(),
|
||||||
|
val error: String? = null
|
||||||
|
)
|
||||||
|
@HiltViewModel
|
||||||
|
class ContactListViewModel @Inject constructor(
|
||||||
|
private val repository: FriendsRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(ContactUiState())
|
||||||
|
val uiState: StateFlow<ContactUiState> = _uiState.asStateFlow()
|
||||||
private val _friends = MutableStateFlow<List<Friend>>(emptyList())
|
private val _friends = MutableStateFlow<List<Friend>>(emptyList())
|
||||||
val friends: StateFlow<List<Friend>> = _friends
|
val friends: StateFlow<List<Friend>> = _friends
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// 畫面一建立就自動抓取資料
|
// 畫面一建立就自動抓取資料
|
||||||
fetchFriends()
|
fetchFriends()
|
||||||
|
fetchPendingRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//friends list
|
||||||
fun fetchFriends() {
|
fun fetchFriends() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
repository.getFriendList()
|
repository.getFriendList()
|
||||||
.onSuccess { list ->
|
.onSuccess { list ->
|
||||||
_friends.value = list
|
_uiState.update { it.copy(isLoading = false, friends = list) }
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure { error ->
|
||||||
// 這裡可以處理錯誤訊息,例如顯示 Toast
|
_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 { /* 處理錯誤 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue