Профиль, редактирование без аватара

This commit is contained in:
Халимов Рустам
2026-04-16 15:41:22 +03:00
parent 8409c51842
commit cea3f4d669
45 changed files with 1063 additions and 178 deletions

View File

@@ -1,5 +1,6 @@
package core.di
import com.google.gson.GsonBuilder
import core.network.AuthInterceptor
import core.network.DynamicBaseUrlInterceptor
import core.network.ServerConfig
@@ -25,7 +26,7 @@ object NetworkModule {
dynamicBaseUrlInterceptor: DynamicBaseUrlInterceptor
): OkHttpClient {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
@@ -40,10 +41,13 @@ object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
val gson = com.google.gson.GsonBuilder()
.serializeNulls()
.create()
return Retrofit.Builder()
.baseUrl("https://api.placeholder.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
}

View File

@@ -5,10 +5,15 @@ import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@Singleton
class AuthInterceptor @Inject constructor(
private val tokenManager: TokenManager
private val tokenManager: TokenManager,
private val navigationManager: core.utils.NavigationManager,
private val authRepositoryProvider: javax.inject.Provider<auth.domain.repository.AuthRepository>
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenManager.getToken()
@@ -17,6 +22,17 @@ class AuthInterceptor @Inject constructor(
addHeader("Authorization", "Bearer $it")
}
}.build()
return chain.proceed(request)
val response = chain.proceed(request)
if (response.code == 401) {
// Global logout
kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.Main) {
authRepositoryProvider.get().logout()
navigationManager.logout()
}
}
return response
}
}

View File

@@ -11,7 +11,18 @@ class ActiveChatTracker @Inject constructor() {
private val _currentChatId = MutableStateFlow<String?>(null)
val currentChatId: StateFlow<String?> = _currentChatId.asStateFlow()
private val _totalUnreadCount = MutableStateFlow(0)
val totalUnreadCount: StateFlow<Int> = _totalUnreadCount.asStateFlow()
fun setChatId(chatId: String?) {
_currentChatId.value = chatId
}
fun setTotalUnreadCount(count: Int) {
_totalUnreadCount.value = count
}
fun incrementUnreadCount() {
_totalUnreadCount.value += 1
}
}

View File

@@ -23,6 +23,9 @@ class ForkFirebaseMessagingService : FirebaseMessagingService() {
@Inject
lateinit var authApi: AuthApi
@Inject
lateinit var activeChatTracker: ActiveChatTracker
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
@@ -48,11 +51,11 @@ class ForkFirebaseMessagingService : FirebaseMessagingService() {
val chatId = message.data["chatId"]
val messageId = message.data["id"] ?: message.messageId
NotificationHelper.showNotification(this, title, body, type, chatId, messageId?.hashCode())
NotificationHelper.showNotification(this, title, body, type, chatId, messageId?.hashCode(), activeChatTracker.totalUnreadCount.value)
}
private fun showNotification(title: String?, body: String?, type: String?) {
NotificationHelper.showNotification(this, title, body, type)
NotificationHelper.showNotification(this, title, body, type, totalCount = activeChatTracker.totalUnreadCount.value)
}
override fun onDestroy() {

View File

@@ -11,7 +11,7 @@ import androidx.core.app.NotificationCompat
object NotificationHelper {
private const val CHANNEL_ID = "fork_notifications_channel"
fun showNotification(context: Context, title: String?, body: String?, type: String?, chatId: String? = null, notificationId: Int? = null) {
fun showNotification(context: Context, title: String?, body: String?, type: String?, chatId: String? = null, notificationId: Int? = null, totalCount: Int = 0) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val finalNotificationId = notificationId ?: (System.currentTimeMillis() % Int.MAX_VALUE).toInt()
@@ -48,6 +48,7 @@ object NotificationHelper {
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setNumber(totalCount)
.setContentIntent(pendingIntent)
notificationManager.notify(finalNotificationId, notificationBuilder.build())

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import androidx.compose.foundation.shape.RoundedCornerShape
import core.presentation.theme.SoftSquareShape
@Composable
@@ -45,10 +46,14 @@ fun AppAvatar(
val avatarColor = Color(0xFF4AA6F3) // Premium Telegram Blue (Web)
val cornerRadius = remember(size) {
if (size <= 32.dp) (size.value * 0.25).dp else 12.dp
}
Box(
modifier = modifier
.size(size)
.clip(SoftSquareShape)
.clip(RoundedCornerShape(cornerRadius))
.background(avatarColor),
contentAlignment = Alignment.Center
) {

View File

@@ -130,17 +130,11 @@ private fun RowScope.ProfileNavItem(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(26.dp)
.clip(CircleShape)
) {
AppAvatar(
url = avatarUrl,
name = username,
size = 26.dp
)
}
AppAvatar(
url = avatarUrl,
name = username,
size = 26.dp
)
Text(
text = stringResource(R.string.profile_tab),
color = contentColor,

View File

@@ -9,11 +9,12 @@ data class LinkMetadata(
object LinkParser {
private val URL_PATTERN = Regex(
"(?:^|[\\s])((https?://)[\\w-]+(\\.[\\w-]+)+\\.?(:\\d+)?(/[\\w- ./?%&=]*)?)",
"(?:^|[\\s])(https?://[^\\s\\n\\r\"]+)",
RegexOption.IGNORE_CASE
)
fun findLinks(text: String): List<String> {
if (text.isEmpty()) return emptyList()
return URL_PATTERN.findAll(text).map { it.groupValues[1].trim() }.toList()
}
}

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
sealed class NavEvent {
data class OpenChat(val chatId: String) : NavEvent()
object Logout : NavEvent()
}
@Singleton
@@ -16,4 +17,8 @@ class NavigationManager @Inject constructor() {
fun navigateToChat(chatId: String) {
_events.tryEmit(NavEvent.OpenChat(chatId))
}
fun logout() {
_events.tryEmit(NavEvent.Logout)
}
}