forked from melod1n/fast-messenger
feat: add channel message support and refactor UI components
- Implement `VkChannelMessage` domain and data models for channel message attachments - Add `CHANNEL_MESSAGE` to `AttachmentType` and map it to relevant UI resources and strings - Refactor `Sticker` and `Gift` composables to accept URL strings instead of domain objects for better decoupling - Simplify `AppTheme` by removing redundant color animations and passing the color scheme directly to `MaterialExpressiveTheme` - Update `LoginScreen` to include a theme toggle (classic vs. light) and improve back-button behavior by resetting error states - Bump VK API version from 5.238 to 5.263 - Adjust layout and padding for sticker and gift attachment previews
This commit is contained in:
@@ -4,7 +4,7 @@ object AppConstants {
|
|||||||
|
|
||||||
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
|
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
|
||||||
|
|
||||||
const val API_VERSION = "5.238"
|
const val API_VERSION = "5.263"
|
||||||
const val URL_OAUTH = "https://oauth.vk.ru"
|
const val URL_OAUTH = "https://oauth.vk.ru"
|
||||||
const val URL_API = "https://api.vk.ru/method"
|
const val URL_API = "https://api.vk.ru/method"
|
||||||
|
|
||||||
|
|||||||
@@ -598,6 +598,7 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
|
|||||||
AttachmentType.VIDEO_MESSAGE -> null
|
AttachmentType.VIDEO_MESSAGE -> null
|
||||||
AttachmentType.GROUP_CHAT_STICKER -> R.drawable.ic_sticker_fill_round_24
|
AttachmentType.GROUP_CHAT_STICKER -> R.drawable.ic_sticker_fill_round_24
|
||||||
AttachmentType.STICKER_PACK_PREVIEW -> null
|
AttachmentType.STICKER_PACK_PREVIEW -> null
|
||||||
|
AttachmentType.CHANNEL_MESSAGE -> null
|
||||||
}?.let(UiImage::Resource)
|
}?.let(UiImage::Resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,6 +688,7 @@ fun getAttachmentUiText(
|
|||||||
AttachmentType.VIDEO_MESSAGE -> R.string.message_attachments_video_message
|
AttachmentType.VIDEO_MESSAGE -> R.string.message_attachments_video_message
|
||||||
AttachmentType.GROUP_CHAT_STICKER -> R.string.message_attachments_group_sticker
|
AttachmentType.GROUP_CHAT_STICKER -> R.string.message_attachments_group_sticker
|
||||||
AttachmentType.STICKER_PACK_PREVIEW -> R.string.message_attachments_sticker_pack_preview
|
AttachmentType.STICKER_PACK_PREVIEW -> R.string.message_attachments_sticker_pack_preview
|
||||||
|
AttachmentType.CHANNEL_MESSAGE -> R.string.message_attachments_channel_message
|
||||||
}.let(UiText::Resource)
|
}.let(UiText::Resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ enum class AttachmentType(var value: String) {
|
|||||||
ARTICLE("article"),
|
ARTICLE("article"),
|
||||||
VIDEO_MESSAGE("video_message"),
|
VIDEO_MESSAGE("video_message"),
|
||||||
GROUP_CHAT_STICKER("ugc_sticker"),
|
GROUP_CHAT_STICKER("ugc_sticker"),
|
||||||
STICKER_PACK_PREVIEW("sticker_pack_preview")
|
STICKER_PACK_PREVIEW("sticker_pack_preview"),
|
||||||
|
CHANNEL_MESSAGE("channel_message")
|
||||||
;
|
;
|
||||||
|
|
||||||
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
|
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ data class VkAttachmentItemData(
|
|||||||
@Json(name = "article") val article: VkArticleData?,
|
@Json(name = "article") val article: VkArticleData?,
|
||||||
@Json(name = "video_message") val videoMessage: VkVideoMessageData?,
|
@Json(name = "video_message") val videoMessage: VkVideoMessageData?,
|
||||||
@Json(name = "ugc_sticker") val groupSticker: VkGroupStickerData?,
|
@Json(name = "ugc_sticker") val groupSticker: VkGroupStickerData?,
|
||||||
@Json(name = "sticker_pack_preview") val stickerPackPreview: VkStickerPackPreviewData?
|
@Json(name = "sticker_pack_preview") val stickerPackPreview: VkStickerPackPreviewData?,
|
||||||
|
@Json(name = "channel_message") val channelMessageData: VkChannelMessageData?
|
||||||
) {
|
) {
|
||||||
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
|
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
|
||||||
AttachmentType.UNKNOWN -> VkUnknownAttachment
|
AttachmentType.UNKNOWN -> VkUnknownAttachment
|
||||||
@@ -66,5 +67,6 @@ data class VkAttachmentItemData(
|
|||||||
AttachmentType.VIDEO_MESSAGE -> videoMessage?.toDomain()
|
AttachmentType.VIDEO_MESSAGE -> videoMessage?.toDomain()
|
||||||
AttachmentType.GROUP_CHAT_STICKER -> groupSticker?.toDomain()
|
AttachmentType.GROUP_CHAT_STICKER -> groupSticker?.toDomain()
|
||||||
AttachmentType.STICKER_PACK_PREVIEW -> stickerPackPreview?.toDomain()
|
AttachmentType.STICKER_PACK_PREVIEW -> stickerPackPreview?.toDomain()
|
||||||
|
AttachmentType.CHANNEL_MESSAGE -> channelMessageData?.toDomain()
|
||||||
} ?: VkUnknownAttachment
|
} ?: VkUnknownAttachment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.meloda.fast.model.api.data
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import dev.meloda.fast.model.api.domain.VkChannelMessage
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class VkChannelMessageData(
|
||||||
|
@Json(name = "channel_id") val channelId: Long,
|
||||||
|
@Json(name = "cmid") val cmId: Long,
|
||||||
|
@Json(name = "author_id") val authorId: Long,
|
||||||
|
@Json(name = "channel_info") val channelInfo: ChannelInfo,
|
||||||
|
@Json(name = "channel_type") val channelType: String,
|
||||||
|
@Json(name = "guid") val guid: String,
|
||||||
|
@Json(name = "text") val text: String?,
|
||||||
|
@Json(name = "time") val time: Long,
|
||||||
|
@Json(name = "attachments") val attachments: List<VkAttachmentItemData> = emptyList(),
|
||||||
|
) : VkAttachmentData {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChannelInfo(
|
||||||
|
@Json(name = "photo_base") val photoBase: String?,
|
||||||
|
@Json(name = "title") val title: String
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toDomain(): VkChannelMessage = VkChannelMessage(
|
||||||
|
channelId = channelId,
|
||||||
|
cmId = cmId,
|
||||||
|
authorId = authorId,
|
||||||
|
channelInfo = VkChannelMessage.ChannelInfo(
|
||||||
|
title = channelInfo.title,
|
||||||
|
photoBase = channelInfo.photoBase
|
||||||
|
),
|
||||||
|
channelType = channelType,
|
||||||
|
guid = guid,
|
||||||
|
text = text,
|
||||||
|
time = time,
|
||||||
|
attachments = attachments.map(VkAttachmentItemData::toDomain),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package dev.meloda.fast.model.api.domain
|
||||||
|
|
||||||
|
import dev.meloda.fast.model.api.data.AttachmentType
|
||||||
|
|
||||||
|
data class VkChannelMessage(
|
||||||
|
val channelId: Long,
|
||||||
|
val cmId: Long,
|
||||||
|
val authorId: Long,
|
||||||
|
val channelInfo: ChannelInfo,
|
||||||
|
val channelType: String,
|
||||||
|
val guid: String,
|
||||||
|
val text: String?,
|
||||||
|
val time: Long,
|
||||||
|
val attachments: List<VkAttachment>?,
|
||||||
|
) : VkAttachment {
|
||||||
|
|
||||||
|
data class ChannelInfo(
|
||||||
|
val title: String,
|
||||||
|
val photoBase: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
override val type: AttachmentType = AttachmentType.CHANNEL_MESSAGE
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ fun AppTheme(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val colorScheme: ColorScheme = when {
|
val colorScheme: ColorScheme = predefinedColorScheme ?: when {
|
||||||
useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
if (useDarkTheme) dynamicDarkColorScheme(context)
|
if (useDarkTheme) dynamicDarkColorScheme(context)
|
||||||
else dynamicLightColorScheme(context)
|
else dynamicLightColorScheme(context)
|
||||||
@@ -82,10 +82,6 @@ fun AppTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val colorPrimary by animateColorAsState(colorScheme.primary)
|
|
||||||
val colorSurface by animateColorAsState(colorScheme.surface)
|
|
||||||
val colorBackground by animateColorAsState(colorScheme.background)
|
|
||||||
|
|
||||||
val typography = if (useSystemFont) {
|
val typography = if (useSystemFont) {
|
||||||
MaterialTheme.typography
|
MaterialTheme.typography
|
||||||
} else {
|
} else {
|
||||||
@@ -118,12 +114,7 @@ fun AppTheme(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MaterialExpressiveTheme(
|
MaterialExpressiveTheme(
|
||||||
colorScheme = (predefinedColorScheme ?: colorScheme)
|
colorScheme = colorScheme,
|
||||||
.copy(
|
|
||||||
primary = colorPrimary,
|
|
||||||
background = colorBackground,
|
|
||||||
surface = colorSurface
|
|
||||||
),
|
|
||||||
typography = typography,
|
typography = typography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
<string name="message_attachments_video_message">Video message</string>
|
<string name="message_attachments_video_message">Video message</string>
|
||||||
<string name="message_attachments_group_sticker">Group sticker</string>
|
<string name="message_attachments_group_sticker">Group sticker</string>
|
||||||
<string name="message_attachments_sticker_pack_preview">Sticker pack preview</string>
|
<string name="message_attachments_sticker_pack_preview">Sticker pack preview</string>
|
||||||
|
<string name="message_attachments_channel_message">Channel message</string>
|
||||||
|
|
||||||
<string name="chat_interaction_uploading_file">Uploading file</string>
|
<string name="chat_interaction_uploading_file">Uploading file</string>
|
||||||
<string name="chat_interaction_uploading_photo">Uploading photo</string>
|
<string name="chat_interaction_uploading_photo">Uploading photo</string>
|
||||||
|
|||||||
@@ -100,7 +100,13 @@ class LoginViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onBackPressed() {
|
fun onBackPressed() {
|
||||||
_screenState.setValue { old -> old.copy(showLogo = true) }
|
_screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
showLogo = true,
|
||||||
|
loginError = false,
|
||||||
|
passwordError = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPasswordVisibilityButtonClicked() {
|
fun onPasswordVisibilityButtonClicked() {
|
||||||
|
|||||||
+17
-2
@@ -31,9 +31,13 @@ import androidx.compose.material3.ScaffoldDefaults
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
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.autofill.ContentType
|
import androidx.compose.ui.autofill.ContentType
|
||||||
@@ -58,7 +62,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.meloda.fast.auth.login.LoginViewModel
|
import dev.meloda.fast.auth.login.LoginViewModel
|
||||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
|
||||||
import dev.meloda.fast.auth.login.model.LoginDialog
|
import dev.meloda.fast.auth.login.model.LoginDialog
|
||||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
@@ -67,6 +70,8 @@ import dev.meloda.fast.ui.R
|
|||||||
import dev.meloda.fast.ui.common.LocalSizeConfig
|
import dev.meloda.fast.ui.common.LocalSizeConfig
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||||
|
import dev.meloda.fast.ui.theme.AppTheme
|
||||||
|
import dev.meloda.fast.ui.theme.ClassicColorScheme
|
||||||
import dev.meloda.fast.ui.util.handleEnterKey
|
import dev.meloda.fast.ui.util.handleEnterKey
|
||||||
import dev.meloda.fast.ui.util.handleTabKey
|
import dev.meloda.fast.ui.util.handleTabKey
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@@ -114,6 +119,12 @@ fun LoginRoute(
|
|||||||
viewModel.onValidationCodeReceived(validationCode)
|
viewModel.onValidationCodeReceived(validationCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var useClassic by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
AppTheme(
|
||||||
|
predefinedColorScheme = if (useClassic) ClassicColorScheme.lightScheme
|
||||||
|
else lightColorScheme(),
|
||||||
|
) {
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
onLoginInputChanged = viewModel::onLoginInputChanged,
|
onLoginInputChanged = viewModel::onLoginInputChanged,
|
||||||
@@ -122,9 +133,13 @@ fun LoginRoute(
|
|||||||
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
||||||
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
||||||
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
||||||
onLogoClicked = viewModel::onLogoClicked,
|
onLogoClicked = {
|
||||||
|
viewModel.onLogoClicked()
|
||||||
|
useClassic = !useClassic
|
||||||
|
},
|
||||||
onLogoLongClicked = onNavigateToSettings
|
onLogoLongClicked = onNavigateToSettings
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
loginDialog = loginDialog,
|
loginDialog = loginDialog,
|
||||||
|
|||||||
+5
-2
@@ -119,12 +119,15 @@ fun Attachments(
|
|||||||
|
|
||||||
AttachmentType.STICKER -> {
|
AttachmentType.STICKER -> {
|
||||||
Sticker(
|
Sticker(
|
||||||
item = attachment as VkStickerDomain
|
url = (attachment as VkStickerDomain).getUrl(
|
||||||
|
width = 256,
|
||||||
|
withBackground = false
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachmentType.GIFT -> {
|
AttachmentType.GIFT -> {
|
||||||
Gift(item = attachment as VkGiftDomain)
|
Gift(url = (attachment as VkGiftDomain).getDefaultThumbSizeOrLess())
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachmentType.VIDEO_MESSAGE -> {
|
AttachmentType.VIDEO_MESSAGE -> {
|
||||||
|
|||||||
+6
-7
@@ -21,25 +21,24 @@ import androidx.compose.ui.res.vectorResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import dev.meloda.fast.model.api.domain.VkGiftDomain
|
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Gift(
|
fun Gift(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: VkGiftDomain
|
url: String
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.width(192.dp),
|
modifier = modifier
|
||||||
|
.width(208.dp)
|
||||||
|
.padding(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = item.getDefaultThumbSizeOrLess(),
|
model = url,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(192.dp)
|
||||||
.padding(8.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
+6
-10
@@ -10,27 +10,23 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import dev.meloda.fast.model.api.domain.VkStickerDomain
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Sticker(
|
fun Sticker(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: VkStickerDomain
|
url: String?
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.size(192.dp),
|
modifier = modifier
|
||||||
|
.size(208.dp)
|
||||||
|
.padding(8.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = item.getUrl(
|
model = url,
|
||||||
width = 256,
|
|
||||||
withBackground = false
|
|
||||||
),
|
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.padding(8.dp)
|
|
||||||
.fillMaxSize()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user