5 Commits

Author SHA1 Message Date
melod1n 389d3f9e52 Style: Update message bubble colors
Updates the container colors for incoming and outgoing message bubbles to align with Material 3 design tokens.

- The outgoing message bubble container color is changed from `surfaceColorAtElevation(2.dp)` to `surfaceContainer`.
- The reply container color within an outgoing message is changed from `primaryContainer` to `surfaceContainerHighest`.

Additionally, the `@Preview` for `MessageBubble` is updated to display both an incoming and an outgoing message for better design validation.
2025-12-15 23:08:42 +03:00
melod1n 69a50f8fcd Refactor: Encapsulate MessageBubble colors
This commit refactors the `MessageBubble` composable by extracting the color logic into a private `messageBubbleColors` function. This function returns an immutable `MessageBubbleColors` data class, which holds the container, content, and reply container colors.

This change cleans up the main composable, improves readability, and centralizes color definitions for both incoming and outgoing messages. Additionally, the background color logic for attachments has been simplified to make it transparent for media types like stickers and videos.
2025-12-15 23:01:38 +03:00
melod1n 8839015249 Fix: Ensure sender's name truncates correctly in incoming messages
This commit resolves an issue where the sender's name in incoming message bubbles would not truncate properly, potentially breaking the layout. The fix ensures the name text correctly adapts to the width of the message bubble.

Additionally, this change introduces `ImmutableList` for message attachments to improve performance and refactors where the conversion to `ImmutableList` happens, moving it into the `MessageMapper`.

Key changes:
- The `MessageBubble` now reports its width, allowing the sender's name `Text` to be constrained correctly.
- Sender's name now uses `labelMedium` typography.
- Enabled showing the sender's name by default in `MessagesHistoryViewModelImpl`.
- Changed `UiItem.Message.attachments` from `List` to `ImmutableList` for better Compose performance.
- Moved the `toImmutableList()` conversion for attachments into the `MessageMapper`.
2025-12-15 22:58:50 +03:00
melod1n 478639e427 Refactor: Extract RootScreen from MainActivity and fix reply message user
This commit refactors the UI composition logic by extracting it from `MainActivity` into a new, dedicated `RootScreen` composable. This improves the separation of concerns and simplifies `MainActivity`.

Additionally, a bug has been fixed where a replied-to message would incorrectly display the author of the parent message instead of its own author.

Key changes:
- Moved theme setup, permission handling, Long-Poll/Online service management, and navigation graph hosting into the new `RootScreen.kt`.
- `MainActivity` is now significantly simplified, delegating its UI composition to `RootScreen`.
- Corrected the user and group assignment for `replyMessage` in `MessagesRepositoryImpl` to ensure the correct author is displayed.
- Introduced `OnlineFriendsViewModel` to the `FriendsRoute` to separate the logic for online friends.
- Replaced `List` with a custom `ImmutableList` for `photoViewerInfo` state to improve Compose stability.
2025-12-15 22:24:17 +03:00
dependabot[bot] dcbfd43896 Chore(deps): Bump actions/upload-artifact from 5 to 6 (#249)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 21:39:44 +03:00
14 changed files with 473 additions and 398 deletions
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
- name: Upload APK with original name - name: Upload APK with original name
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: ${{ env.APK_NAME }} name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }} path: ${{ env.APK_PATH }}
@@ -56,7 +56,7 @@ jobs:
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
- name: Upload APK with original name - name: Upload APK with original name
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: ${{ env.APK_NAME }} name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }} path: ${{ env.APK_PATH }}
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
run: ./gradlew assembleRelease run: ./gradlew assembleRelease
- name: Upload release APK - name: Upload release APK
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: app-release.apk name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk path: app/build/outputs/apk/release/app-release.apk
@@ -43,7 +43,7 @@ jobs:
run: ./gradlew bundleRelease run: ./gradlew bundleRelease
- name: Upload release Bundle - name: Upload release Bundle
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: app-release.aab name: app-release.aab
path: app/build/outputs/bundle/release/app-release.aab path: app/build/outputs/bundle/release/app-release.aab
@@ -4,7 +4,6 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
@@ -15,43 +14,20 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.conena.nanokt.android.content.pxToDp
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.service.OnlineService import dev.meloda.fast.service.OnlineService
import dev.meloda.fast.service.longpolling.LongPollingService import dev.meloda.fast.service.longpolling.LongPollingService
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalSizeConfig
import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig
import dev.meloda.fast.ui.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -88,168 +64,26 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermissions() requestNotificationPermissions()
setContent { setContent {
val resources = LocalResources.current
val userSettings: UserSettings = koinInject()
val longPollController: LongPollController = koinInject()
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LaunchedEffect(viewModel) {
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() Log.d("VM_CREATE", "onCreate: viewModel: $viewModel")
}
LifecycleResumeEffect(true) { LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent) viewModel.onAppResumed(intent)
onPauseOrDispose {} onPauseOrDispose {}
} }
val permissionState = RootScreen(
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) toggleLongPollService = { enable, inBackground ->
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToCheckPermission) {
if (isNeedToCheckPermission) {
viewModel.onPermissionCheckStatus(permissionState.status)
if (permissionState.status.isGranted) {
if (longPollCurrentState == LongPollState.InApp) {
toggleLongPollService(false)
}
toggleLongPollService( toggleLongPollService(
enable = true, enable = enable,
inBackground = true inBackground = inBackground ?: AppSettings.Experimental.longPollInBackground
) )
} },
} toggleOnlineService = ::toggleOnlineService
}
LaunchedEffect(isNeedToRequestPermission) {
if (isNeedToRequestPermission) {
viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
LifecycleResumeEffect(longPollStateToApply) {
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false)
Log.d("LongPoll", "recreate()")
}
toggleLongPollService(
enable = longPollStateToApply.isLaunched(),
inBackground = longPollStateToApply == LongPollState.Background
) )
} }
onPauseOrDispose {}
}
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
LifecycleResumeEffect(sendOnline) {
toggleOnlineService(sendOnline)
onPauseOrDispose {
toggleOnlineService(false)
}
}
val deviceWidthDp = remember(true) {
resources.displayMetrics.widthPixels.pxToDp()
}
val deviceHeightDp = remember(true) {
resources.displayMetrics.heightPixels.pxToDp()
}
val deviceWidthSize by remember(deviceWidthDp) {
derivedStateOf {
when {
deviceWidthDp <= 360 -> DeviceSize.Small
deviceWidthDp <= 600 -> DeviceSize.Compact
deviceWidthDp <= 840 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val deviceHeightSize by remember(deviceHeightDp) {
derivedStateOf {
when {
deviceHeightDp <= 480 -> DeviceSize.Small
deviceHeightDp <= 700 -> DeviceSize.Compact
deviceHeightDp <= 900 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
mutableStateOf(
SizeConfig(
widthSize = deviceWidthSize,
heightSize = deviceHeightSize
)
)
}
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
val themeConfig by remember(
darkMode,
dynamicColors,
amoledDark,
enableBlur,
enableMultiline,
setDarkMode,
useSystemFont
) {
derivedStateOf {
ThemeConfig(
darkMode = setDarkMode,
dynamicColors = dynamicColors,
selectedColorScheme = 0,
amoledDark = amoledDark,
enableBlur = enableBlur,
enableMultiline = enableMultiline,
useSystemFont = useSystemFont,
enableAnimations = enableAnimations
)
}
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,
useDynamicColors = themeConfig.dynamicColors,
selectedColorScheme = themeConfig.selectedColorScheme,
useAmoledBackground = themeConfig.amoledDark,
useSystemFont = themeConfig.useSystemFont
) {
RootScreen(viewModel = viewModel)
}
}
}
} }
private fun createNotificationChannels() { private fun createNotificationChannels() {
@@ -279,7 +113,7 @@ class MainActivity : AppCompatActivity() {
} }
val notificationManager: NotificationManager = val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannels( notificationManager.createNotificationChannels(
listOf( listOf(
@@ -1,8 +1,10 @@
package dev.meloda.fast.presentation package dev.meloda.fast.presentation
import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -15,44 +17,225 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.conena.nanokt.android.content.pxToDp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModel
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.authNavGraph
import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.auth.navigateToAuth
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.conversations.navigation.createChatScreen import dev.meloda.fast.conversations.navigation.createChatScreen
import dev.meloda.fast.conversations.navigation.navigateToCreateChat import dev.meloda.fast.conversations.navigation.navigateToCreateChat
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.Main
import dev.meloda.fast.navigation.mainScreen import dev.meloda.fast.navigation.mainScreen
import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog
import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.navigateToSettings
import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.LocalSizeConfig
import dev.meloda.fast.ui.model.DeviceSize
import dev.meloda.fast.ui.model.SizeConfig
import dev.meloda.fast.ui.model.ThemeConfig
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.theme.LocalNavController import dev.meloda.fast.ui.theme.LocalNavController
import dev.meloda.fast.ui.theme.LocalNavRootController import dev.meloda.fast.ui.theme.LocalNavRootController
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import dev.meloda.fast.ui.util.immutableListOf
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun RootScreen( fun RootScreen(
navController: NavHostController = rememberNavController(), toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit,
viewModel: MainViewModel toggleOnlineService: (enable: Boolean) -> Unit
) { ) {
val resources = LocalResources.current
val userSettings: UserSettings = koinInject()
val longPollController: LongPollController = koinInject()
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
LaunchedEffect(viewModel) {
Log.d("VM_CREATE", "RootScreen(): viewModel: $viewModel")
}
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
val permissionState =
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToCheckPermission) {
if (isNeedToCheckPermission) {
viewModel.onPermissionCheckStatus(permissionState.status)
if (permissionState.status.isGranted) {
if (longPollCurrentState == LongPollState.InApp) {
toggleLongPollService(false, null)
}
toggleLongPollService(true, true)
}
}
}
LaunchedEffect(isNeedToRequestPermission) {
if (isNeedToRequestPermission) {
viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
LifecycleResumeEffect(longPollStateToApply) {
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false, null)
Log.d("LongPoll", "recreate()")
}
toggleLongPollService(
longPollStateToApply.isLaunched(),
longPollStateToApply == LongPollState.Background
)
}
onPauseOrDispose {}
}
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
LifecycleResumeEffect(sendOnline) {
toggleOnlineService(sendOnline)
onPauseOrDispose {
toggleOnlineService(false)
}
}
val deviceWidthDp = remember(true) {
resources.displayMetrics.widthPixels.pxToDp()
}
val deviceHeightDp = remember(true) {
resources.displayMetrics.heightPixels.pxToDp()
}
val deviceWidthSize by remember(deviceWidthDp) {
derivedStateOf {
when {
deviceWidthDp <= 360 -> DeviceSize.Small
deviceWidthDp <= 600 -> DeviceSize.Compact
deviceWidthDp <= 840 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val deviceHeightSize by remember(deviceHeightDp) {
derivedStateOf {
when {
deviceHeightDp <= 480 -> DeviceSize.Small
deviceHeightDp <= 700 -> DeviceSize.Compact
deviceHeightDp <= 900 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
mutableStateOf(
SizeConfig(
widthSize = deviceWidthSize,
heightSize = deviceHeightSize
)
)
}
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
val themeConfig by remember(
darkMode,
dynamicColors,
amoledDark,
enableBlur,
enableMultiline,
setDarkMode,
useSystemFont
) {
derivedStateOf {
ThemeConfig(
darkMode = setDarkMode,
dynamicColors = dynamicColors,
selectedColorScheme = 0,
amoledDark = amoledDark,
enableBlur = enableBlur,
enableMultiline = enableMultiline,
useSystemFont = useSystemFont,
enableAnimations = enableAnimations
)
}
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,
useDynamicColors = themeConfig.dynamicColors,
selectedColorScheme = themeConfig.selectedColorScheme,
useAmoledBackground = themeConfig.amoledDark,
useSystemFont = themeConfig.useSystemFont
) {
val navController: NavHostController = rememberNavController()
val activity = LocalActivity.current val activity = LocalActivity.current
val context = LocalContext.current val context = LocalContext.current
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle() val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
@@ -126,7 +309,7 @@ fun RootScreen(
LocalNavController provides navController LocalNavController provides navController
) { ) {
var photoViewerInfo by rememberSaveable { var photoViewerInfo by rememberSaveable {
mutableStateOf<Pair<List<String>, Int?>?>(null) mutableStateOf<Pair<ImmutableList<String>, Int?>?>(null)
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@@ -149,7 +332,9 @@ fun RootScreen(
onError = viewModel::onError, onError = viewModel::onError,
onSettingsButtonClicked = navController::navigateToSettings, onSettingsButtonClicked = navController::navigateToSettings,
onNavigateToMessagesHistory = navController::navigateToMessagesHistory, onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null }, onPhotoClicked = { url ->
photoViewerInfo = immutableListOf(url) to null
},
onMessageClicked = navController::navigateToMessagesHistory, onMessageClicked = navController::navigateToMessagesHistory,
onNavigateToCreateChat = navController::navigateToCreateChat onNavigateToCreateChat = navController::navigateToCreateChat
) )
@@ -159,12 +344,14 @@ fun RootScreen(
onBack = navController::navigateUp, onBack = navController::navigateUp,
onNavigateToChatMaterials = navController::navigateToChatMaterials, onNavigateToChatMaterials = navController::navigateToChatMaterials,
onNavigateToPhotoViewer = { photos, index -> onNavigateToPhotoViewer = { photos, index ->
photoViewerInfo = photos to index photoViewerInfo = photos.toImmutableList() to index
} }
) )
chatMaterialsScreen( chatMaterialsScreen(
onBack = navController::navigateUp, onBack = navController::navigateUp,
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null } onPhotoClicked = { url ->
photoViewerInfo = immutableListOf(url) to null
}
) )
createChatScreen( createChatScreen(
onChatCreated = { conversationId -> onChatCreated = { conversationId ->
@@ -198,6 +385,8 @@ fun RootScreen(
} }
} }
} }
}
}
fun NavController.navigateToMain() { fun NavController.navigateToMain() {
this.navigate(Main) { this.navigate(Main) {
@@ -92,12 +92,14 @@ class MessagesRepositoryImpl(
group = groupsMap.messageGroup(message), group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message), actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message), actionGroup = groupsMap.messageActionGroup(message),
replyMessage = message.replyMessage?.copy( replyMessage = message.replyMessage.let { replyMessage ->
user = usersMap.messageUser(message), replyMessage?.copy(
group = groupsMap.messageGroup(message), user = usersMap.messageUser(replyMessage),
actionUser = usersMap.messageActionUser(message), group = groupsMap.messageGroup(replyMessage),
actionGroup = groupsMap.messageActionGroup(message), actionUser = usersMap.messageActionUser(replyMessage),
actionGroup = groupsMap.messageActionGroup(replyMessage),
) )
}
).also { VkMemoryCache[message.id] = it } ).also { VkMemoryCache[message.id] = it }
} }
} }
@@ -167,12 +169,14 @@ class MessagesRepositoryImpl(
group = groupsMap.messageGroup(message), group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message), actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message), actionGroup = groupsMap.messageActionGroup(message),
replyMessage = message.replyMessage?.asDomain()?.copy( replyMessage = message.replyMessage?.asDomain().let { replyMessage ->
user = usersMap.messageUser(message), replyMessage?.copy(
group = groupsMap.messageGroup(message), user = usersMap.messageUser(replyMessage),
actionUser = usersMap.messageActionUser(message), group = groupsMap.messageGroup(replyMessage),
actionGroup = groupsMap.messageActionGroup(message), actionUser = usersMap.messageActionUser(replyMessage),
actionGroup = groupsMap.messageActionGroup(replyMessage),
) )
}
) )
} }
@@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import dev.meloda.fast.friends.FriendsViewModel import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
import dev.meloda.fast.friends.presentation.FriendsRoute import dev.meloda.fast.friends.presentation.FriendsRoute
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -20,14 +21,14 @@ fun NavGraphBuilder.friendsScreen(
onMessageClicked: (userId: Long) -> Unit, onMessageClicked: (userId: Long) -> Unit,
onScrolledToTop: () -> Unit onScrolledToTop: () -> Unit
) { ) {
val friendsViewModel: FriendsViewModel = with(activity) { val friendsViewModel: FriendsViewModel = activity.getViewModel<FriendsViewModelImpl>()
getViewModel<FriendsViewModelImpl>() val onlineFriendsViewModel =
} activity.getViewModel<OnlineFriendsViewModelImpl>()
composable<Friends> { composable<Friends> {
FriendsRoute( FriendsRoute(
activity = activity,
friendsViewModel = friendsViewModel, friendsViewModel = friendsViewModel,
onlineFriendsViewModel = onlineFriendsViewModel,
onError = onError, onError = onError,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked,
@@ -1,6 +1,5 @@
package dev.meloda.fast.friends.presentation package dev.meloda.fast.friends.presentation
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@@ -54,17 +53,16 @@ import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.getViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@Composable @Composable
fun FriendsRoute( fun FriendsRoute(
activity: AppCompatActivity,
friendsViewModel: FriendsViewModel, friendsViewModel: FriendsViewModel,
onlineFriendsViewModel: OnlineFriendsViewModelImpl,
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userid: Long) -> Unit, onMessageClicked: (userid: Long) -> Unit,
onScrolledToTop: () -> Unit onScrolledToTop: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -232,9 +230,7 @@ fun FriendsRoute(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { index -> ) { index ->
FriendsScreen( FriendsScreen(
viewModel = if (index == 0) friendsViewModel else with(activity) { viewModel = if (index == 0) friendsViewModel else onlineFriendsViewModel,
getViewModel<OnlineFriendsViewModelImpl>()
},
orderType = orderType, orderType = orderType,
padding = padding, padding = padding,
tabIndex = index, tabIndex = index,
@@ -1231,7 +1231,7 @@ class MessagesHistoryViewModelImpl(
val newUiMessages = messages.mapIndexed { index, message -> val newUiMessages = messages.mapIndexed { index, message ->
message.asPresentation( message.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
showName = false, showName = true,
prevMessage = messages.getOrNull(index + 1), prevMessage = messages.getOrNull(index + 1),
nextMessage = messages.getOrNull(index - 1), nextMessage = messages.getOrNull(index - 1),
showTimeInActionMessages = AppSettings.Experimental.showTimeInActionMessages, showTimeInActionMessages = AppSettings.Experimental.showTimeInActionMessages,
@@ -1,14 +1,17 @@
package dev.meloda.fast.messageshistory.model package dev.meloda.fast.messageshistory.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.ui.util.ImmutableList
sealed class UiItem( sealed class UiItem(
open val id: Long, open val id: Long,
open val cmId: Long open val cmId: Long
) { ) {
@Stable
data class Message( data class Message(
override val id: Long, override val id: Long,
override val cmId: Long, override val cmId: Long,
@@ -29,12 +32,13 @@ sealed class UiItem(
val isSelected: Boolean, val isSelected: Boolean,
val isPinned: Boolean, val isPinned: Boolean,
val isImportant: Boolean, val isImportant: Boolean,
val attachments: List<VkAttachment>?, val attachments: ImmutableList<VkAttachment>?,
val replyCmId: Long?, val replyCmId: Long?,
val replyTitle: String?, val replyTitle: String?,
val replySummary: String? val replySummary: String?
) : UiItem(id, cmId) ) : UiItem(id, cmId)
@Stable
data class ActionMessage( data class ActionMessage(
override val id: Long, override val id: Long,
override val cmId: Long, override val cmId: Long,
@@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -58,7 +60,8 @@ fun MessageBubble(
replySummary: String? = null, replySummary: String? = null,
onClick: (VkAttachment) -> Unit = {}, onClick: (VkAttachment) -> Unit = {},
onLongClick: (VkAttachment) -> Unit = {}, onLongClick: (VkAttachment) -> Unit = {},
onReplyClick: () -> Unit = {} onReplyClick: () -> Unit = {},
onBubbleWidthChange: (Int) -> Unit = {},
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
@@ -66,22 +69,7 @@ fun MessageBubble(
val currentOnLongClick by rememberUpdatedState(onLongClick) val currentOnLongClick by rememberUpdatedState(onLongClick)
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
val backgroundColor = if (!isOut) { val colors = messageBubbleColors(isOut = isOut)
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
} else {
MaterialTheme.colorScheme.primaryContainer
}
val replyBackgroundColor = if (!isOut) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.inversePrimary
}
val contentColor = if (!isOut) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onPrimaryContainer
}
val shouldShowBubble = !text.isNullOrEmpty() val shouldShowBubble = !text.isNullOrEmpty()
@@ -115,7 +103,7 @@ fun MessageBubble(
label = "dateContainerWidth" label = "dateContainerWidth"
) )
CompositionLocalProvider(LocalContentColor provides contentColor) { CompositionLocalProvider(LocalContentColor provides colors.content) {
Column( Column(
modifier = modifier modifier = modifier
.wrapContentWidth() .wrapContentWidth()
@@ -138,8 +126,8 @@ fun MessageBubble(
onClick = onReplyClick, onClick = onReplyClick,
title = replyTitle, title = replyTitle,
summary = replySummary, summary = replySummary,
backgroundColor = backgroundColor, backgroundColor = colors.container,
innerBackgroundColor = replyBackgroundColor innerBackgroundColor = colors.replyContainer
) )
} }
@@ -149,6 +137,9 @@ fun MessageBubble(
.onGloballyPositioned { .onGloballyPositioned {
bubbleContainerWidth = it.size.width bubbleContainerWidth = it.size.width
} }
.onSizeChanged {
onBubbleWidthChange(it.width)
}
.widthIn(min = if (shouldFill) attachmentsContainerWidth.dp else 56.dp) .widthIn(min = if (shouldFill) attachmentsContainerWidth.dp else 56.dp)
.clip( .clip(
RoundedCornerShape( RoundedCornerShape(
@@ -158,7 +149,7 @@ fun MessageBubble(
bottomEnd = if (attachments != null) 0.dp else 24.dp bottomEnd = if (attachments != null) 0.dp else 24.dp
) )
) )
.background(backgroundColor) .background(colors.container)
.padding( .padding(
start = 8.dp, start = 8.dp,
end = 8.dp, end = 8.dp,
@@ -199,6 +190,15 @@ fun MessageBubble(
} }
if (attachments != null) { if (attachments != null) {
val firstAttachment = attachments.firstOrNull()
val isMediaAttachment = firstAttachment is VkStickerDomain ||
firstAttachment is VkVideoMessageDomain
val attachmentBackgroundColor = if (isMediaAttachment) {
Color.Transparent
} else {
colors.container
}
Box( Box(
modifier = Modifier modifier = Modifier
.onGloballyPositioned { .onGloballyPositioned {
@@ -213,18 +213,7 @@ fun MessageBubble(
topEnd = 0.dp topEnd = 0.dp
) )
) )
.background( .background(attachmentBackgroundColor)
backgroundColor.copy(
alpha = if ((attachments.firstOrNull()?.javaClass
?: Nothing::class.java)
in listOf(
VkStickerDomain::class.java,
VkVideoMessageDomain::class.java
)
) 0f
else 1f
)
)
) { ) {
Attachments( Attachments(
modifier = Modifier, modifier = Modifier,
@@ -236,7 +225,7 @@ fun MessageBubble(
val dateStatusBackground = if (theme.darkMode) Color.Black.copy(alpha = 0.5f) val dateStatusBackground = if (theme.darkMode) Color.Black.copy(alpha = 0.5f)
else Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.5f)
CompositionLocalProvider(LocalContentColor provides contentColor) { CompositionLocalProvider(LocalContentColor provides colors.content) {
DateStatus( DateStatus(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
@@ -261,9 +250,34 @@ fun MessageBubble(
} }
} }
@Immutable
private data class MessageBubbleColors(
val container: Color,
val content: Color,
val replyContainer: Color,
)
@Composable
private fun messageBubbleColors(isOut: Boolean): MessageBubbleColors {
return if (isOut) {
MessageBubbleColors(
container = MaterialTheme.colorScheme.primaryContainer,
content = MaterialTheme.colorScheme.onPrimaryContainer,
replyContainer = MaterialTheme.colorScheme.inversePrimary
)
} else {
MessageBubbleColors(
container = MaterialTheme.colorScheme.surfaceContainer,
content = MaterialTheme.colorScheme.onSurface,
replyContainer = MaterialTheme.colorScheme.surfaceContainerHighest
)
}
}
@Preview @Preview
@Composable @Composable
private fun Bubble() { private fun Bubble() {
Column {
MessageBubble( MessageBubble(
modifier = Modifier, modifier = Modifier,
text = AnnotatedString("Some cool text"), text = AnnotatedString("Some cool text"),
@@ -281,4 +295,23 @@ private fun Bubble() {
onClick = {}, onClick = {},
onLongClick = {}, onLongClick = {},
) )
MessageBubble(
modifier = Modifier,
text = AnnotatedString("Some cool text"),
isOut = false,
date = "19:01",
isEdited = true,
isRead = true,
sendingStatus = SendingStatus.SENT,
isPinned = true,
isImportant = true,
isSelected = false,
attachments = emptyImmutableList(),
replyTitle = "Danil Nikolaev",
replySummary = "2 photos",
onClick = {},
onLongClick = {},
)
}
} }
@@ -20,12 +20,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
@@ -36,7 +40,6 @@ import com.conena.nanokt.android.content.dpInPx
import dev.meloda.fast.messageshistory.model.UiItem import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
@@ -49,10 +52,16 @@ fun IncomingMessageBubble(
onLongClick: (VkAttachment) -> Unit = {}, onLongClick: (VkAttachment) -> Unit = {},
onReplyClick: () -> Unit = {} onReplyClick: () -> Unit = {}
) { ) {
val density = LocalDensity.current
val currentOnClick by rememberUpdatedState(onClick) val currentOnClick by rememberUpdatedState(onClick)
val currentOnLongClick by rememberUpdatedState(onLongClick) val currentOnLongClick by rememberUpdatedState(onLongClick)
val currentOnReplyClick by rememberUpdatedState(onReplyClick) val currentOnReplyClick by rememberUpdatedState(onReplyClick)
var bubbleContainerWidth by remember {
mutableStateOf(0.dp)
}
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -103,9 +112,12 @@ fun IncomingMessageBubble(
Text( Text(
modifier = Modifier modifier = Modifier
.padding(start = 12.dp) .padding(start = 12.dp)
.widthIn(max = 140.dp), .widthIn(
max = (bubbleContainerWidth.takeIf { it > 0.dp }
?: 140.dp) - 24.dp
),
text = message.name, text = message.name,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@@ -123,14 +135,16 @@ fun IncomingMessageBubble(
isPinned = message.isPinned, isPinned = message.isPinned,
isImportant = message.isImportant, isImportant = message.isImportant,
isSelected = message.isSelected, isSelected = message.isSelected,
attachments = message.attachments?.toImmutableList(), attachments = message.attachments,
replyTitle = message.replyTitle, replyTitle = message.replyTitle,
replySummary = message.replySummary, replySummary = message.replySummary,
onClick = currentOnClick, onClick = currentOnClick,
onLongClick = currentOnLongClick, onLongClick = currentOnLongClick,
onReplyClick = currentOnReplyClick onReplyClick = currentOnReplyClick,
onBubbleWidthChange = {
bubbleContainerWidth = with(density) { it.toDp() }
}
) )
} }
} }
Spacer(modifier = Modifier.fillMaxWidth(0.25f)) Spacer(modifier = Modifier.fillMaxWidth(0.25f))
@@ -80,7 +80,7 @@ fun OutgoingMessageBubble(
isPinned = message.isPinned, isPinned = message.isPinned,
isImportant = message.isImportant, isImportant = message.isImportant,
isSelected = message.isSelected, isSelected = message.isSelected,
attachments = message.attachments?.toImmutableList(), attachments = message.attachments,
replyTitle = message.replyTitle, replyTitle = message.replyTitle,
replySummary = message.replySummary, replySummary = message.replySummary,
onClick = currentOnClick, onClick = currentOnClick,
@@ -24,6 +24,7 @@ import dev.meloda.fast.model.api.domain.FormatDataType
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@@ -156,7 +157,7 @@ fun VkMessage.asPresentation(
isSelected = isSelected, isSelected = isSelected,
isPinned = isPinned, isPinned = isPinned,
isImportant = isImportant, isImportant = isImportant,
attachments = attachments?.ifEmpty { null }, attachments = attachments?.ifEmpty { null }?.toImmutableList(),
replyCmId = replyMessage?.cmId, replyCmId = replyMessage?.cmId,
replyTitle = extractReplyTitle(), replyTitle = extractReplyTitle(),
replySummary = extractReplySummary() replySummary = extractReplySummary()
@@ -1,6 +1,5 @@
package dev.meloda.fast.photoviewer.presentation package dev.meloda.fast.photoviewer.presentation
import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
@@ -59,9 +58,9 @@ import dev.meloda.fast.photoviewer.model.PhotoViewScreenState
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.FullScreenDialog import dev.meloda.fast.ui.components.FullScreenDialog
import dev.meloda.fast.ui.components.Loader import dev.meloda.fast.ui.components.Loader
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.getImage import dev.meloda.fast.ui.util.getImage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.net.URLEncoder import java.net.URLEncoder
@@ -69,7 +68,7 @@ import kotlin.math.abs
@Composable @Composable
fun PhotoViewDialog( fun PhotoViewDialog(
photoViewerInfo: Pair<List<String>, Int?>?, photoViewerInfo: Pair<ImmutableList<String>, Int?>?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
@@ -85,7 +84,7 @@ fun PhotoViewDialog(
arguments = PhotoViewArguments( arguments = PhotoViewArguments(
imageUrls = photoViewerInfo.first.map { imageUrls = photoViewerInfo.first.map {
URLEncoder.encode(it, "utf-8") URLEncoder.encode(it, "utf-8")
}, }.toList(),
selectedIndex = photoViewerInfo.second selectedIndex = photoViewerInfo.second
), ),
applicationContext = applicationContext applicationContext = applicationContext