move all ui-related classes and files to ui module

This commit is contained in:
2024-07-15 18:31:58 +03:00
parent 9a1bce5707
commit ee7499f117
171 changed files with 405 additions and 1354 deletions
@@ -1,721 +0,0 @@
package com.meloda.app.fast.common.util
//import android.content.Context
//import androidx.compose.ui.graphics.Color
//import androidx.compose.ui.text.AnnotatedString
//import androidx.compose.ui.text.SpanStyle
//import androidx.compose.ui.text.buildAnnotatedString
//import androidx.compose.ui.text.font.FontWeight
//import androidx.compose.ui.text.withStyle
//import com.meloda.app.fast.common.UiImage
//import com.meloda.app.fast.common.UiText
//import com.meloda.app.fast.common.extensions.orDots
//import com.meloda.app.fast.common.parseString
//
//
//@Suppress("MemberVisibilityCanBePrivate")
//object VkUtils {
//
// fun prepareMessageText(text: String, forConversations: Boolean = false): String {
// return text.apply {
// if (forConversations) {
// replace("\n", " ")
// }
//
// replace("&", "&")
// replace(""", "\"")
// replace("<br>", "\n")
// replace("&gt;", ">")
// replace("&lt;", "<")
// replace("<br/>", "\n")
// replace("&ndash;", "-")
// trim()
// }
// }
//
// fun parseAttachments(baseAttachments: List<VkAttachmentItemData>?): List<VkAttachment>? {
// if (baseAttachments.isNullOrEmpty()) return null
//
// val attachments = mutableListOf<VkAttachment>()
//
// for (baseAttachment in baseAttachments) {
// when (baseAttachment.getPreparedType()) {
// AttachmentType.UNKNOWN -> continue
//
// AttachmentType.PHOTO -> {
// val photo = baseAttachment.photo ?: continue
// attachments += photo.toDomain()
// }
//
// AttachmentType.VIDEO -> {
// val video = baseAttachment.video ?: continue
// attachments += video.toDomain()
// }
//
// AttachmentType.AUDIO -> {
// val audio = baseAttachment.audio ?: continue
// attachments += audio.toDomain()
// }
//
// AttachmentType.FILE -> {
// val file = baseAttachment.file ?: continue
// attachments += file.toDomain()
// }
//
// AttachmentType.LINK -> {
// val link = baseAttachment.link ?: continue
// attachments += link.toDomain()
// }
//
// AttachmentType.MINI_APP -> {
// val miniApp = baseAttachment.miniApp ?: continue
// attachments += miniApp.toDomain()
// }
//
// AttachmentType.AUDIO_MESSAGE -> {
// val voiceMessage = baseAttachment.voiceMessage ?: continue
// attachments += voiceMessage.toDomain()
// }
//
// AttachmentType.STICKER -> {
// val sticker = baseAttachment.sticker ?: continue
// attachments += sticker.toDomain()
// }
//
// AttachmentType.GIFT -> {
// val gift = baseAttachment.gift ?: continue
// attachments += gift.toDomain()
// }
//
// AttachmentType.WALL -> {
// val wall = baseAttachment.wall ?: continue
// attachments += wall.toDomain()
// }
//
// AttachmentType.GRAFFITI -> {
// val graffiti = baseAttachment.graffiti ?: continue
// attachments += graffiti.toDomain()
// }
//
// AttachmentType.POLL -> {
// val poll = baseAttachment.poll ?: continue
// attachments += poll.toDomain()
// }
//
// AttachmentType.WALL_REPLY -> {
// val wallReply = baseAttachment.wallReply ?: continue
// attachments += wallReply.toDomain()
// }
//
// AttachmentType.CALL -> {
// val call = baseAttachment.call ?: continue
// attachments += call.toDomain()
// }
//
// AttachmentType.GROUP_CALL_IN_PROGRESS -> {
// val groupCall = baseAttachment.groupCall ?: continue
// attachments += groupCall.toDomain()
// }
//
// AttachmentType.CURATOR -> {
// val curator = baseAttachment.curator ?: continue
// attachments += curator.toDomain()
// }
//
// AttachmentType.EVENT -> {
// val event = baseAttachment.event ?: continue
// attachments += event.toDomain()
// }
//
// AttachmentType.STORY -> {
// val story = baseAttachment.story ?: continue
// attachments += story.toDomain()
// }
//
// AttachmentType.WIDGET -> {
// val widget = baseAttachment.widget ?: continue
// attachments += widget.toDomain()
// }
//
// AttachmentType.ARTIST -> {
// val artist = baseAttachment.artist ?: continue
// attachments += artist.toDomain()
//
// val audios = baseAttachment.audios ?: continue
// audios.map(VkAudioData::toDomain).let(attachments::addAll)
// }
//
// AttachmentType.AUDIO_PLAYLIST -> {
// val audioPlaylist = baseAttachment.audioPlaylist ?: continue
// attachments += audioPlaylist.toDomain()
// }
//
// AttachmentType.PODCAST -> {
// val podcast = baseAttachment.podcast ?: continue
// attachments += podcast.toDomain()
// }
// }
// }
//
// return attachments
// }
//
// fun getActionMessageText(
// context: Context,
// message: VkMessage?,
// youPrefix: String,
// messageUser: VkUserDomain?,
// messageGroup: VkGroupDomain?,
// action: VkMessage.Action?,
// actionUser: VkUserDomain?,
// actionGroup: VkGroupDomain?,
// ): AnnotatedString? {
// return when {
// message == null -> null
// action == null -> null
//
// else -> buildAnnotatedString {
// when (action) {
// VkMessage.Action.CHAT_CREATE -> {
// val text = message.actionText ?: return null
//
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// val string = UiText.ResourceParams(
// UiR.string.message_action_chat_created,
// listOf(prefix, text)
// ).parseString(context).orEmpty()
//
// append(string)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
//
// val textStartIndex = string.indexOf(text)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = textStartIndex,
// end = textStartIndex + text.length
// )
// }
//
// VkMessage.Action.CHAT_TITLE_UPDATE -> {
// val text = message.actionText ?: return null
//
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// val string = UiText.ResourceParams(
// UiR.string.message_action_chat_renamed,
// listOf(prefix, text)
// ).parseString(context).orEmpty()
//
// append(string)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
//
// val textStartIndex = string.indexOf(text)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = textStartIndex,
// end = textStartIndex + text.length
// )
// }
//
// VkMessage.Action.CHAT_PHOTO_UPDATE -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_photo_update,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_PHOTO_REMOVE -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_photo_remove,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_KICK_USER -> {
// val memberId = message.actionMemberId ?: return null
// val isUser = memberId > 0
// val isGroup = memberId < 0
//
// if (isUser && actionUser == null) return null
// if (isGroup && actionGroup == null) return null
//
// if (memberId == message.fromId) {
// val prefix =
// if (memberId == UserConfig.userId) youPrefix
// else actionUser.toString()
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_user_left,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// } else {
// val prefix =
// if (message.fromId == UserConfig.userId) youPrefix
// else messageUser?.toString() ?: messageGroup?.toString().orDots()
//
// val postfix =
// if (memberId == UserConfig.userId) youPrefix.lowercase()
// else actionUser.toString()
//
// val string = UiText.ResourceParams(
// UiR.string.message_action_chat_user_kicked,
// listOf(prefix, postfix)
// ).parseString(context).orEmpty()
//
// append(string)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
//
// val postfixStartIndex = string.indexOf(postfix)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = postfixStartIndex,
// end = postfixStartIndex + postfix.length
// )
// }
// }
//
// VkMessage.Action.CHAT_INVITE_USER -> {
// val memberId = message.actionMemberId ?: 0
// val isUser = memberId > 0
// val isGroup = memberId < 0
//
// if (isUser && actionUser == null) return null
// if (isGroup && actionGroup == null) return null
//
// if (memberId == message.fromId) {
// val prefix =
// if (memberId == UserConfig.userId) youPrefix
// else actionUser.toString()
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_user_returned,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// } else {
// val prefix =
// if (message.fromId == UserConfig.userId) youPrefix
// else messageUser?.toString() ?: messageGroup?.toString().orDots()
//
// val postfix =
// if (memberId == UserConfig.userId) youPrefix.lowercase()
// else actionUser.toString()
//
// val string = UiText.ResourceParams(
// UiR.string.message_action_chat_user_invited,
// listOf(prefix, postfix)
// ).parseString(context).orEmpty()
//
// append(string)
//
// val postfixStartIndex = string.indexOf(postfix)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = postfixStartIndex,
// end = postfixStartIndex + postfix.length
// )
// }
// }
//
// VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_user_joined_by_link,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_user_joined_by_call,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_user_joined_by_call_link,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_PIN_MESSAGE -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_pin_message,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_unpin_message,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_SCREENSHOT -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isGroup() -> messageGroup?.name
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_screenshot,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
//
// VkMessage.Action.CHAT_STYLE_UPDATE -> {
// val prefix = when {
// message.fromId == UserConfig.userId -> youPrefix
// message.isUser() -> messageUser?.toString()
// else -> return null
// } ?: return null
//
// UiText.ResourceParams(
// UiR.string.message_action_chat_style_update,
// listOf(prefix)
// ).parseString(context).orEmpty().let(::append)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.SemiBold),
// start = 0,
// end = prefix.length
// )
// }
// }
// }
// }
// }
//
// fun getForwardsText(context: Context, message: VkMessage?): AnnotatedString? {
// return when {
// message == null -> null
//
// message.hasForwards() -> buildAnnotatedString {
// val forwards = message.forwards.orEmpty()
//
// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
// append(
// UiText.Resource(
// if (forwards.size == 1) UiR.string.forwarded_message
// else UiR.string.forwarded_messages
// ).parseString(context)
// )
// }
// }
//
// else -> null
// }
// }
//
// fun getAttachmentText(
// getText: (UiText) -> String,
// message: VkMessage?
// ): AnnotatedString? {
// return when {
// message == null -> null
//
// message.geoType != null -> buildAnnotatedString {
// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
// when (message.geoType) {
// "point" -> getText(UiText.Resource(UiR.string.message_geo_point))
// .let(::append)
//
// else -> getText(UiText.Resource(UiR.string.message_geo))
// .let(::append)
// }
// }
// }
//
// message.hasAttachments() -> buildAnnotatedString {
// val attachments = message.attachments.orEmpty()
//
// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
// if (attachments.size == 1) {
// getText(getAttachmentUiText(attachments.first())).let(::append)
// } else {
// when {
// isAttachmentsHaveOneType(attachments) -> {
// getText(getAttachmentUiText(attachments.first(), attachments.size))
// .let(::append)
// }
//
// attachments.any { it.type == AttachmentType.ARTIST } -> {
// getText(
// getAttachmentUiText(attachments.first { it.type == AttachmentType.ARTIST })
// ).let(::append)
// }
//
// else -> {
// getText(UiText.Resource(UiR.string.message_attachments_many))
// .let(::append)
// }
// }
// }
// }
// }
//
// else -> null
// }
// }
//
// fun getAttachmentConversationIcon(message: VkMessage?): UiImage? {
// return message?.attachments?.let { attachments ->
// if (attachments.isEmpty()) return null
// if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
// message.geoType?.let {
// return UiImage.Resource(UiR.drawable.ic_map_marker)
// }
//
// getAttachmentIconByType(attachments.first().type)
// } else {
// UiImage.Resource(UiR.drawable.ic_baseline_attach_file_24)
// }
// }
// }
//
//
//
// fun getAttachmentUiText(
// attachment: VkAttachment,
// size: Int = 1,
// ): UiText {
// if (attachment.type.isMultiple()) {
// return when (attachment.type) {
// AttachmentType.PHOTO -> UiR.plurals.attachment_photos
// AttachmentType.VIDEO -> UiR.plurals.attachment_videos
// AttachmentType.AUDIO -> UiR.plurals.attachment_audios
// AttachmentType.FILE -> UiR.plurals.attachment_files
// else -> throw IllegalArgumentException("Unknown multiple type: ${attachment.type}")
// }.let { resId -> UiText.QuantityResource(resId, size) }
// }
//
// return when (attachment.type) {
// AttachmentType.UNKNOWN,
// AttachmentType.PHOTO,
// AttachmentType.VIDEO,
// AttachmentType.AUDIO,
// AttachmentType.FILE -> {
// throw IllegalArgumentException("Unknown multiple type: ${attachment.type}")
// }
//
// AttachmentType.LINK -> UiR.string.message_attachments_link
// AttachmentType.AUDIO_MESSAGE -> UiR.string.message_attachments_audio_message
// AttachmentType.MINI_APP -> UiR.string.message_attachments_mini_app
// AttachmentType.STICKER -> UiR.string.message_attachments_sticker
// AttachmentType.GIFT -> UiR.string.message_attachments_gift
// AttachmentType.WALL -> UiR.string.message_attachments_wall
// AttachmentType.GRAFFITI -> UiR.string.message_attachments_graffiti
// AttachmentType.POLL -> UiR.string.message_attachments_poll
// AttachmentType.WALL_REPLY -> UiR.string.message_attachments_wall_reply
// AttachmentType.CALL -> UiR.string.message_attachments_call
// AttachmentType.GROUP_CALL_IN_PROGRESS -> UiR.string.message_attachments_call_in_progress
// AttachmentType.CURATOR -> UiR.string.message_attachments_curator
// AttachmentType.EVENT -> UiR.string.message_attachments_event
// AttachmentType.STORY -> UiR.string.message_attachments_story
// AttachmentType.WIDGET -> UiR.string.message_attachments_widget
// AttachmentType.ARTIST -> UiR.string.message_attachments_artist
// AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist
// AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
// }.let(UiText::Resource)
// }
//
// fun getTextWithVisualizedMentions(
// originalText: String,
// mentionColor: Color,
// ): AnnotatedString = buildAnnotatedString {
// val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
//
// val mentions = mutableListOf<MentionIndex>()
//
// var currentIndex = 0
// val replacements = mutableListOf<Pair<IntRange, String>>()
//
// // TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня)
// val result = regex.replace(originalText) { matchResult ->
// val idPrefix = matchResult.groups[1]?.value.orEmpty()
// val startIndex = matchResult.range.first
// val endIndex = matchResult.range.last
//
// val id = matchResult.groups[2]?.value ?: ""
// val text = matchResult.groups[3]?.value ?: ""
//
// val replaced =
// text.substring(startIndex, endIndex + 1)
// .replace("[$idPrefix$id|$text]", text)
//
// val indexRange =
// (startIndex + currentIndex)..startIndex + currentIndex + replaced.length
//
// replacements.add(indexRange to replaced)
//
// mentions += MentionIndex(
// id = id.toIntOrNull() ?: -1,
// idPrefix = idPrefix,
// indexRange = indexRange
// )
//
// currentIndex += replaced.length - (endIndex - startIndex + 1)
//
// replaced
// }
//
// append(result)
//
// mentions.forEach { mention ->
// val startIndex = mention.indexRange.first
// val endIndex = mention.indexRange.last
//
// addStyle(
// style = SpanStyle(color = mentionColor),
// start = startIndex,
// end = endIndex
// )
// addStringAnnotation(
// tag = mention.idPrefix,
// annotation = mention.id.toString(),
// start = startIndex,
// end = endIndex
// )
// }
// }
//
//
//}
@@ -2,10 +2,8 @@ package com.meloda.app.fast.data
import com.meloda.app.fast.network.OAuthErrorDomain
import com.meloda.app.fast.network.RestApiErrorDomain
import com.meloda.app.fast.network.VkErrorCode
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
sealed class State<out T> {
@@ -16,7 +14,7 @@ sealed class State<out T> {
sealed class Error : State<Nothing>() {
data class ApiError(
val errorCode: Int,
val errorCode: VkErrorCode,
val errorMessage: String,
) : Error()
@@ -61,19 +59,9 @@ inline fun <T> State<T>.processState(
}
}
inline fun <T, R> Flow<State<T>>.mapSuccess(
crossinline transform: suspend (value: T) -> R
): Flow<R> = filterIsInstance<State.Success<T>>()
.map { state -> transform.invoke(state.data) }
fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) {
null -> State.Error.ConnectionError
else -> State.Error.ApiError(code, message)
}
fun OAuthErrorDomain?.toStateApiError(): State.Error = when (this) {
null -> State.Error.ConnectionError
else -> State.Error.OAuthError(this)
else -> State.Error.ApiError(VkErrorCode.parse(code), message)
}
fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
@@ -1,18 +1,12 @@
package com.meloda.app.fast.data.api.auth
import com.meloda.app.fast.model.api.requests.AuthDirectRequest
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
import com.meloda.app.fast.model.api.responses.SendSmsResponse
import com.meloda.app.fast.network.OAuthErrorDomain
import com.meloda.app.fast.model.api.responses.ValidatePhoneResponse
import com.meloda.app.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface AuthRepository {
// suspend fun auth(
// params: AuthDirectRequest
// ): ApiResult<AuthDirectResponse, OAuthErrorDomain>
suspend fun sendSms(
suspend fun validatePhone(
validationSid: String
): SendSmsResponse
): ApiResult<ValidatePhoneResponse, RestApiErrorDomain>
}
@@ -1,35 +1,20 @@
package com.meloda.app.fast.data.api.auth
import com.meloda.app.fast.model.api.requests.AuthDirectRequest
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
import com.meloda.app.fast.model.api.responses.SendSmsResponse
import com.meloda.app.fast.network.OAuthErrorDomain
import com.meloda.app.fast.model.api.responses.ValidatePhoneResponse
import com.meloda.app.fast.network.RestApiErrorDomain
import com.meloda.app.fast.network.mapApiDefault
import com.meloda.app.fast.network.service.auth.AuthService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AuthRepositoryImpl(
private val authService: AuthService
private val service: AuthService
) : AuthRepository {
// override suspend fun auth(
// params: AuthDirectRequest
// ): ApiResult<AuthDirectResponse, OAuthErrorDomain> {
//
// }
// TODO: 05/05/2024, Danil Nikolaev: implement
override suspend fun sendSms(
override suspend fun validatePhone(
validationSid: String
): SendSmsResponse = withContext(Dispatchers.IO) {
SendSmsResponse(
validationSid = null, delay = null, validationType = null, validationResend = null
)
// authService.sendSms(validationSid).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
service.validatePhone(validationSid).mapApiDefault()
}
}
@@ -6,7 +6,7 @@ import com.meloda.app.fast.model.api.requests.LongPollGetUpdatesRequest
import com.meloda.app.fast.model.api.requests.MessagesGetLongPollServerRequest
import com.meloda.app.fast.network.RestApiErrorDomain
import com.meloda.app.fast.network.mapApiResult
import com.meloda.app.fast.network.mapResult
import com.meloda.app.fast.network.mapDefault
import com.meloda.app.fast.network.service.longpoll.LongPollService
import com.meloda.app.fast.network.service.messages.MessagesService
import com.slack.eithernet.ApiResult
@@ -49,9 +49,7 @@ class LongPollRepositoryImpl(
mode = mode,
version = version
)
longPollService.getResponse(serverUrl, requestModel.map).mapResult(
successMapper = { response -> response },
errorMapper = { error -> error?.toDomain() }
)
longPollService.getResponse(serverUrl, requestModel.map).mapDefault()
}
}
+1
View File
@@ -29,6 +29,7 @@ android {
dependencies {
api(projects.core.common)
api(projects.core.ui)
implementation(libs.koin.android)
}
@@ -4,7 +4,7 @@ import android.content.res.Resources
import android.os.PowerManager
import android.util.Log
import com.meloda.app.fast.datastore.model.LongPollState
import com.meloda.app.fast.datastore.model.ThemeConfig
import com.meloda.app.fast.ui.model.ThemeConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
+3 -2
View File
@@ -36,16 +36,17 @@ android {
}
dependencies {
// TODO: 05/05/2024, Danil Nikolaev: maybe remove
implementation(projects.core.common)
implementation(projects.core.datastore)
implementation(projects.core.ui)
implementation(libs.appcompat)
implementation(libs.accompanist.permissions)
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.haze)
implementation(libs.haze.materials)
debugImplementation(libs.compose.ui.tooling)
}
@@ -4,9 +4,18 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SendSmsResponse(
data class ValidatePhoneResponse(
@Json(name = "sid") val validationSid: String?,
@Json(name = "delay") val delay: Int?,
@Json(name = "validation_type") val validationType: String?,
@Json(name = "validation_resend") val validationResend: String?
)
@JsonClass(generateAdapter = true)
data class ValidateLoginResponse(
@Json(name = "result") val result: String,
@Json(name = "sid") val sid: String,
@Json(name = "phone") val phone: String?,
@Json(name = "is_email") val isEmail: Boolean?,
@Json(name = "email_reg_allowed") val emailRegAllowed: Boolean?
)
@@ -3,6 +3,13 @@ package com.meloda.app.fast.model.api.responses
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AuthDirectErrorOnlyResponse(
@Json(name = "error") val error: String,
@Json(name = "error_description") val errorDescription: String?,
@Json(name = "error_type") val errorType: String?
)
@JsonClass(generateAdapter = true)
data class AuthDirectResponse(
@Json(name = "access_token") val accessToken: String?,
@@ -35,3 +42,9 @@ data class AuthDirectResponse(
@Json(name = "restore_url") val restoreUrl: String
)
}
@JsonClass(generateAdapter = true)
data class GetAnonymousTokenResponse(
@Json(name = "token") val token: String,
@Json(name = "expired_at") val expiredAt: Int
)
@@ -32,37 +32,6 @@ fun <Success : Any, Error : Any, SuccessDomain : Any, ErrorDomain : Any>
}
}
fun <Success : Any, Error : Any, SuccessDomain : Any, ErrorDomain : Any>
ApiResult<Success, Error>.mapOAuthResult(
successMapper: (Success) -> SuccessDomain,
errorMapper: (Error?) -> ErrorDomain?
): ApiResult<SuccessDomain, ErrorDomain> {
if (BuildConfig.DEBUG) printStackTraceIfAny()
return when (this) {
is ApiResult.Success -> {
ApiResult.success(successMapper(value))
}
is ApiResult.Failure.NetworkFailure -> {
ApiResult.networkFailure(error)
}
is ApiResult.Failure.UnknownFailure -> {
ApiResult.unknownFailure(error)
}
is ApiResult.Failure.HttpFailure -> {
ApiResult.httpFailure(code, errorMapper(error))
}
is ApiResult.Failure.ApiFailure -> {
ApiResult.apiFailure(errorMapper(error))
}
}
}
fun <Success : ApiResponse<*>, SuccessDomain : Any, ErrorDomain : Any>
ApiResult<Success, RestApiError>.mapApiResult(
successMapper: (Success) -> SuccessDomain,
@@ -86,30 +55,18 @@ fun <Success : ApiResponse<*>, SuccessDomain : Any, ErrorDomain : Any>
}
}
fun <R : Any> ApiResult<R, RestApiError>.mapDefault(): ApiResult<R, RestApiErrorDomain> =
mapResult(
successMapper = { response -> response },
errorMapper = { error -> error?.toDomain() }
)
fun <T : Any, R : ApiResponse<T>> ApiResult<R, RestApiError>.mapApiDefault(): ApiResult<T, RestApiErrorDomain> =
mapResult(
successMapper = { response -> response.requireResponse() },
errorMapper = { error -> error?.toDomain() }
)
//@OptIn(ExperimentalContracts::class)
//inline fun <R : Any, E : Any, C> OAuthResponse<R, E>.fold(
// onSuccess: (value: R) -> C,
// onFailure: (failure: E) -> C,
//): C {
// contract {
// callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
// callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
// }
// return when (this) {
// is OAuthResponse.Success -> onSuccess(response)
// is OAuthResponse.Error -> onFailure(error)
// }
//}
fun <Success : Any, Error : Any> ApiResult<Success, Error>.isSuccess(): Boolean =
this is ApiResult.Success
fun <Success : Any, Error : Any> ApiResult<Success, Error>.printStackTraceIfAny() {
val throwable = when (this) {
is ApiResult.Failure.NetworkFailure -> error
@@ -118,9 +75,3 @@ fun <Success : Any, Error : Any> ApiResult<Success, Error>.printStackTraceIfAny(
}
throwable?.printStackTrace()
}
fun ApiResult.Failure.HttpFailure<*>?.tryCastToRestErrorDomain() =
this?.error as? RestApiErrorDomain
fun ApiResult.Failure.ApiFailure<*>?.tryCastToRestErrorDomain() =
this?.error as? RestApiErrorDomain
@@ -1,156 +0,0 @@
package com.meloda.app.fast.network
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// TODO: 09/05/2024, Danil Nikolaev: reimplement as sealed class
@JsonClass(generateAdapter = true)
open class OAuthError(
@Json(name = "error") open val error: String,
@Json(name = "error_description") open val errorDescription: String?,
@Json(name = "error_type") open val errorType: String?
)
@JsonClass(generateAdapter = true)
data class ValidationRequiredError(
@Json(name = "error") override val error: String, // "need_validation"
@Json(name = "error_description") override val errorDescription: String, // "sms sent, use code param" if sms method; "use app code" if 2fa app
@Json(name = "validation_type") val validationType: String, // 2fa_app, 2sa_sms
@Json(name = "validation_sid") val validationSid: String,
@Json(name = "phone_mask") val phoneMask: String, // "+7 *** *** ** 50"
@Json(name = "redirect_uri") val redirectUri: String,
@Json(name = "validation_resend") val validationResend: String?, // Приходит, если для отправки кода нужно вызвать метод auth.validatePhone
@Json(name = "cant_get_code_open_restore") val restoreIfCannotGetCode: Boolean?
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = null
)
@JsonClass(generateAdapter = true)
data class CaptchaRequiredError(
@Json(name = "error") override val error: String, // "need_captcha"
@Json(name = "captcha_sid") val captchaSid: String,
@Json(name = "captcha_img") val captchaImage: String,
@Json(name = "captcha_ts") val captchaTs: Double?,
@Json(name = "captcha_ratio") val captchaRatio: Double?,
@Json(name = "captcha_track") val captchaTrack: String?,
@Json(name = "is_refresh_enabled") val isRefreshEnabled: Boolean?,
@Json(name = "is_sound_captcha_available") val isSoundCaptchaAvailable: Boolean?
) : OAuthError(
error = error,
errorDescription = null,
errorType = null
)
@JsonClass(generateAdapter = true)
data class UserBannedError(
@Json(name = "error") override val error: String, // need_validation
@Json(name = "error_description") override val errorDescription: String, // user has been banned
@Json(name = "ban_info") val banInfo: BanInfo
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = null
) {
@JsonClass(generateAdapter = true)
data class BanInfo(
@Json(name = "member_name") val memberName: String,
@Json(name = "message") val message: String,
@Json(name = "access_token") val accessToken: String,
@Json(name = "restore_url") val restoreUrl: String
)
}
@JsonClass(generateAdapter = true)
data class InvalidCredentialsError(
@Json(name = "error") override val error: String, // "invalid_client"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "username_or_password_is_incorrect"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class WrongValidationCodeError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "wrong_otp"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class WrongValidationCodeFormatError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
@JsonClass(generateAdapter = true)
data class TooManyTriesError(
@Json(name = "error") override val error: String, // "9;Flood control"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "password_bruteforce_attempt"
) : OAuthError(
error = error,
errorDescription = errorDescription,
errorType = errorType
)
fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
is ValidationRequiredError -> {
OAuthErrorDomain.ValidationRequiredError(
description = errorDescription,
validationType = ValidationType.parse(validationType),
validationSid = validationSid,
phoneMask = phoneMask,
redirectUri = redirectUri,
validationResend = validationResend,
restoreIfCannotGetCode = restoreIfCannotGetCode
)
}
is CaptchaRequiredError -> {
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = captchaSid,
captchaImageUrl = captchaImage
)
}
is UserBannedError -> {
OAuthErrorDomain.UserBannedError(
memberName = banInfo.memberName,
message = banInfo.message,
accessToken = banInfo.accessToken,
restoreUrl = banInfo.restoreUrl
)
}
is InvalidCredentialsError -> {
OAuthErrorDomain.InvalidCredentialsError
}
is WrongValidationCodeError -> {
OAuthErrorDomain.WrongValidationCode
}
is WrongValidationCodeFormatError -> {
OAuthErrorDomain.WrongValidationCodeFormat
}
is TooManyTriesError -> {
OAuthErrorDomain.TooManyTriesError
}
else -> null
}
@@ -2,6 +2,8 @@ package com.meloda.app.fast.network
sealed class OAuthErrorDomain {
data object UnknownError : OAuthErrorDomain()
data class ValidationRequiredError(
val description: String,
val validationType: ValidationType,
@@ -28,6 +30,4 @@ sealed class OAuthErrorDomain {
data object WrongValidationCode : OAuthErrorDomain()
data object WrongValidationCodeFormat : OAuthErrorDomain()
data object TooManyTriesError: OAuthErrorDomain()
data object UnknownError : OAuthErrorDomain()
}
@@ -1,8 +1,6 @@
package com.meloda.app.fast.network
sealed interface OAuthResponse<out R : Any, out E : Any> {
data class Success<out R : Any>(val response: R) : OAuthResponse<R, Nothing>
data class Error<out E : Any>(val error: E?) : OAuthResponse<Nothing, E>
}
@@ -1,6 +1,6 @@
package com.meloda.app.fast.network
import android.util.Log
import com.meloda.app.fast.model.api.responses.AuthDirectErrorOnlyResponse
import com.squareup.moshi.Moshi
import okhttp3.Request
import okio.Timeout
@@ -28,10 +28,10 @@ class OAuthResultCallFactory(private val moshi: Moshi) : CallAdapter.Factory() {
if (getRawType(callInnerType) == OAuthResponse::class.java) {
if (callInnerType is ParameterizedType) {
val resultInnerType = getParameterUpperBound(0, callInnerType)
return ResultCallAdapter<Any, OAuthError>(resultInnerType, moshi)
return ResultCallAdapter<Any>(resultInnerType, moshi)
}
return ResultCallAdapter<Nothing, Nothing>(Nothing::class.java, moshi)
return ResultCallAdapter<Nothing>(Nothing::class.java, moshi)
}
}
}
@@ -60,36 +60,36 @@ internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : C
abstract fun cloneImpl(): Call<Out>
}
private class ResultCallAdapter<R : Any, E : OAuthError>(
private class ResultCallAdapter<R : Any>(
private val type: Type,
private val moshi: Moshi
) : CallAdapter<R, Call<OAuthResponse<R, E>>> {
) : CallAdapter<R, Call<OAuthResponse<R, AuthDirectErrorOnlyResponse>>> {
override fun responseType() = type
override fun adapt(call: Call<R>): Call<OAuthResponse<R, E>> = ResultCall(call, moshi)
override fun adapt(call: Call<R>): Call<OAuthResponse<R, AuthDirectErrorOnlyResponse>> =
ResultCall(call, moshi)
}
internal class ResultCall<R : Any, E : OAuthError>(
internal class ResultCall<R : Any>(
proxy: Call<R>,
private val moshi: Moshi
) : CallDelegate<R, OAuthResponse<R, E>>(proxy) {
) : CallDelegate<R, OAuthResponse<R, AuthDirectErrorOnlyResponse>>(proxy) {
override fun enqueueImpl(callback: Callback<OAuthResponse<R, E>>) {
override fun enqueueImpl(callback: Callback<OAuthResponse<R, AuthDirectErrorOnlyResponse>>) {
proxy.enqueue(ResultCallback(this, callback, moshi))
}
override fun cloneImpl(): ResultCall<R, E> {
override fun cloneImpl(): ResultCall<R> {
return ResultCall(proxy.clone(), moshi)
}
private class ResultCallback<R : Any, E : OAuthError>(
private val proxy: ResultCall<R, E>,
private val callback: Callback<OAuthResponse<R, E>>,
private class ResultCallback<R : Any>(
private val proxy: ResultCall<R>,
private val callback: Callback<OAuthResponse<R, AuthDirectErrorOnlyResponse>>,
private val moshi: Moshi
) : Callback<R> {
@Suppress("UNCHECKED_CAST")
override fun onResponse(call: Call<R>, response: Response<R>) {
when {
response.isSuccessful -> {
@@ -106,92 +106,19 @@ internal class ResultCall<R : Any, E : OAuthError>(
else -> {
val errorBodyString = response.errorBody()?.string()
val baseError: OAuthError = moshi.adapter(OAuthError::class.java)
val baseError = moshi.adapter(AuthDirectErrorOnlyResponse::class.java)
.fromJson(errorBodyString.orEmpty()) ?: return
val error: OAuthError? = when (baseError.error) {
"9;Flood control" -> {
moshi.adapter(TooManyTriesError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"invalid_client" -> {
moshi.adapter(InvalidCredentialsError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"need_captcha" -> {
moshi.adapter(CaptchaRequiredError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"invalid_request" -> {
when (val type = baseError.errorType) {
"wrong_otp" -> {
moshi.adapter(WrongValidationCodeError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"otp_format_is_incorrect" -> {
moshi.adapter(WrongValidationCodeFormatError::class.java)
.fromJson(errorBodyString.orEmpty())
}
else -> {
Log.d(
"ResultCallback",
"onResponse: invalid_request; error_type: $type"
)
error("Unknown type: $type")
}
}
}
"need_validation" -> {
when (val description = baseError.errorDescription) {
"user has been banned" -> {
moshi.adapter(UserBannedError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"sms sent, use code param",
"use app code" -> {
moshi.adapter(ValidationRequiredError::class.java)
.fromJson(errorBodyString.orEmpty())
}
else -> {
Log.d(
"ResultCallback",
"onResponse: need_validation; description: $description"
)
error("Unknown description: $description")
}
}
}
else -> null
}
error?.let {
callback.onResponse(
proxy,
Response.success(OAuthResponse.Error(error) as OAuthResponse<R, E>)
)
}
callback.onResponse(
proxy,
Response.success(OAuthResponse.Error(baseError) as OAuthResponse<R, AuthDirectErrorOnlyResponse>)
)
}
}
}
override fun onFailure(call: Call<R>, error: Throwable) {
val b = error
// TODO: 12/04/2024, Danil Nikolaev: handle
// callback.onResponse(
// proxy,
// Response.success(OAuthAnswer.Error((throwable = error)))
// )
throw error
}
}
@@ -1,9 +1,10 @@
package com.meloda.app.fast.network;
enum class ValidationType(val value: String) {
APP("2fa_app"), SMS("2fa_sms");
APP("2fa_app"),
SMS("2fa_sms");
companion object {
fun parse(value: String): ValidationType = entries.first { it.value == value }
}
}
companion object {
fun parse(value: String): ValidationType = entries.first { it.value == value }
}
}
@@ -0,0 +1,52 @@
package com.meloda.app.fast.network
enum class VkErrorCode(val code: Int) {
UNKNOWN_ERROR(1),
APP_DISABLED(2),
UNKNOWN_METHOD(3),
INVALID_SIGNATURE(4),
USER_AUTHORIZATION_FAILED(5),
TOO_MANY_REQUESTS(6),
NO_RIGHTS(7),
BAD_REQUEST(8),
TOO_MANY_SIMILAR_ACTIONS(9),
INTERNAL_SERVER_ERROR(10),
IN_TEST_MODE(11),
EXECUTE_CODE_COMPILE_ERROR(12),
EXECUTE_CODE_RUNTIME_ERROR(13),
CAPTCHA_NEEDED(14),
ACCESS_DENIED(15),
REQUIRES_REQUESTS_OVER_HTTPS(16),
VALIDATION_REQUIRED(17),
USER_BANNED_OR_DELETED(18),
ACTION_PROHIBITED(20),
ACTION_ALLOWED_ONLY_FOR_STANDALONE(21),
METHOD_OFF(23),
CONFIRMATION_REQUIRED(24),
PARAMETER_IS_NOT_SPECIFIED(100),
INCORRECT_APP_ID(101),
OUT_OF_LIMITS(103),
INCORRECT_USER_ID(113),
INCORRECT_TIMESTAMP(150),
ACCESS_TO_ALBUM_DENIED(200),
ACCESS_TO_AUDIO_DENIED(201),
ACCESS_TO_GROUP_DENIED(203),
ALBUM_IS_FULL(300),
ACTION_DENIED(500),
PERMISSION_DENIED(600),
CANNOT_SEND_MESSAGE_BLACK_LIST(900),
CANNOT_SEND_MESSAGE_GROUP(901),
INVALID_DOC_ID(1150),
INVALID_DOC_TITLE(1152),
ACCESS_TO_DOC_DENIED(1153),
SOME_AUTH_ERROR(104),
ACCESS_TOKEN_EXPIRED(1117);
companion object {
fun parse(code: Int): VkErrorCode = entries.firstOrNull { it.code == code }
?: throw IllegalArgumentException("Unknown error with value: $code")
}
}
@@ -1,62 +0,0 @@
package com.meloda.app.fast.network
@Suppress("unused")
object VkErrorCodes {
const val UnknownError = 1
const val AppDisabled = 2
const val UnknownMethod = 3
const val InvalidSignature = 4
const val UserAuthorizationFailed = 5
const val TooManyRequests = 6
const val NoRights = 7
const val BadRequest = 8
const val TooManySimilarActions = 9
const val InternalServerError = 10
const val InTestMode = 11
const val ExecuteCodeCompileError = 12
const val ExecuteCodeRuntimeError = 13
const val CaptchaNeeded = 14
const val AccessDenied = 15
const val RequiresRequestsOverHttps = 16
const val ValidationRequired = 17
const val UserBannedOrDeleted = 18
const val ActionProhibited = 20
const val ActionAllowedOnlyForStandalone = 21
const val MethodOff = 23
const val ConfirmationRequired = 24
const val ParameterIsNotSpecified = 100
const val IncorrectAppId = 101
const val OutOfLimits = 103
const val IncorrectUserId = 113
const val IncorrectTimestamp = 150
const val AccessToAlbumDenied = 200
const val AccessToAudioDenied = 201
const val AccessToGroupDenied = 203
const val AlbumIsFull = 300
const val ActionDenied = 500
const val PermissionDenied = 600
const val CannotSendMessageBlackList = 900
const val CannotSendMessageGroup = 901
const val InvalidDocId = 1150
const val InvalidDocTitle = 1152
const val AccessToDocDenied = 1153
const val AccessTokenExpired = 1117
}
object VkOAuthErrors {
const val UNKNOWN = "unknown_error"
const val NEED_VALIDATION = "need_validation"
const val NEED_CAPTCHA = "need_captcha"
const val INVALID_CLIENT = "invalid_client"
const val INVALID_REQUEST = "invalid_request"
const val FLOOD_CONTROL = "9;Flood control"
}
object VkErrorTypes {
const val WRONG_OTP_FORMAT = "otp_format_is_incorrect"
const val WRONG_OTP = "wrong_otp"
const val PASSWORD_BRUTEFORCE_ATTEMPT = "password_bruteforce_attempt"
}
@@ -0,0 +1,16 @@
package com.meloda.app.fast.network
enum class VkOAuthError(val value: String) {
UNKNOWN("unknown_error"),
NEED_VALIDATION("need_validation"),
NEED_CAPTCHA("need_captcha"),
INVALID_CLIENT("invalid_client"),
INVALID_REQUEST("invalid_request"),
FLOOD_CONTROL("9;Flood control");
companion object {
fun parse(value: String): VkOAuthError = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown error with value: $value")
}
}
@@ -0,0 +1,12 @@
package com.meloda.app.fast.network;
enum class VkOAuthErrorType(val value: String) {
WRONG_OTP_FORMAT("otp_format_is_incorrect"),
WRONG_OTP("wrong_otp"),
PASSWORD_BRUTEFORCE_ATTEMPT("password_bruteforce_attempt");
companion object {
fun parse(value: String): VkOAuthErrorType = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown error type with value: $value")
}
}
@@ -1,14 +1,23 @@
package com.meloda.app.fast.network.service.auth
import com.meloda.app.fast.model.api.responses.SendSmsResponse
import com.meloda.app.fast.model.api.responses.ValidateLoginResponse
import com.meloda.app.fast.model.api.responses.ValidatePhoneResponse
import com.meloda.app.fast.network.ApiResponse
import com.meloda.app.fast.network.RestApiError
import com.slack.eithernet.ApiResult
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthService {
@GET(AuthUrls.SEND_SMS)
suspend fun sendSms(@Query("sid") validationSid: String): ApiResult<ApiResponse<SendSmsResponse>, RestApiError>
@GET(AuthUrls.VALIDATE_PHONE)
suspend fun validatePhone(
@Query("sid") validationSid: String
): ApiResult<ApiResponse<ValidatePhoneResponse>, RestApiError>
@GET(AuthUrls.VALIDATE_LOGIN)
suspend fun validateLogin(
@QueryMap param: Map<String, String>
): ApiResult<ApiResponse<ValidateLoginResponse>, RestApiError>
}
@@ -3,6 +3,8 @@ package com.meloda.app.fast.network.service.auth
import com.meloda.app.fast.common.AppConstants
object AuthUrls {
private const val URL = AppConstants.URL_API
const val SEND_SMS = "${AppConstants.URL_API}/auth.validatePhone"
const val VALIDATE_PHONE = "$URL/auth.validatePhone"
const val VALIDATE_LOGIN = "$URL/auth.validateLogin"
}
@@ -1,6 +1,7 @@
package com.meloda.app.fast.network.service.oauth
import com.meloda.app.fast.model.api.responses.AuthDirectResponse
import com.meloda.app.fast.model.api.responses.GetAnonymousTokenResponse
import com.slack.eithernet.ApiResult
import com.slack.eithernet.DecodeErrorBody
import retrofit2.http.GET
@@ -11,6 +12,12 @@ interface OAuthService {
@DecodeErrorBody
@GET(OAuthUrls.DIRECT_AUTH)
suspend fun auth(
@QueryMap param: Map<String, String?>
@QueryMap param: Map<String, String>
): ApiResult<AuthDirectResponse, AuthDirectResponse>
@DecodeErrorBody
@GET(OAuthUrls.GET_ANONYMOUS_TOKEN)
suspend fun getAnonymousToken(
@QueryMap param: Map<String, String>
): ApiResult<GetAnonymousTokenResponse, GetAnonymousTokenResponse>
}
@@ -3,6 +3,8 @@ package com.meloda.app.fast.network.service.oauth
import com.meloda.app.fast.common.AppConstants
object OAuthUrls {
private const val URL = AppConstants.URL_OAUTH
const val DIRECT_AUTH = "${AppConstants.URL_OAUTH}/token"
const val DIRECT_AUTH = "$URL/token"
const val GET_ANONYMOUS_TOKEN = "$URL/get_anonym_token"
}
+6 -1
View File
@@ -32,9 +32,14 @@ android {
}
dependencies {
api(projects.core.designsystem)
implementation(projects.core.common)
api(projects.core.model)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose)
debugImplementation(libs.compose.ui.tooling)
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.basic
import android.os.Build
import android.view.autofill.AutofillManager
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.basic
/*
* Copyright 2020 The Android Open Source Project
@@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.luminance
import com.meloda.app.fast.ui.util.isUsingDarkTheme
/**
* Default alpha levels used by Material components.
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.basic
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@@ -0,0 +1,341 @@
package com.meloda.app.fast.ui.components
// TODO: 26.08.2023, Danil Nikolaev: rewrite
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
const val numberOfDots = 3
val dotSize = 6.dp
val dotColor: Color = Color.Blue
const val delayUnit = 300
const val duration = numberOfDots * delayUnit
val spaceBetween = 2.dp
@Composable
fun DotsPulsing() {
@Composable
fun Dot(scale: Float) {
Spacer(
Modifier
.size(dotSize)
.scale(scale)
.background(
color = dotColor,
shape = CircleShape
)
)
}
val infiniteTransition = rememberInfiniteTransition(label = "")
@Composable
fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = delayUnit * numberOfDots
0f at delay using LinearEasing
1f at delay + delayUnit using LinearEasing
0f at delay + duration
}), label = ""
)
val scales = arrayListOf<State<Float>>()
for (i in 0 until numberOfDots) {
scales.add(animateScaleWithDelay(delay = i * delayUnit))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
scales.forEach {
Dot(it.value)
Spacer(Modifier.width(spaceBetween))
}
}
}
@Composable
fun DotsElastic() {
val minScale = 0.6f
@Composable
fun Dot(scale: Float) {
Spacer(
Modifier
.size(dotSize)
.scale(scaleX = minScale, scaleY = scale)
.background(
color = dotColor,
shape = CircleShape
)
)
}
val infiniteTransition = rememberInfiniteTransition(label = "")
@Composable
fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = minScale,
targetValue = minScale,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = duration
minScale at delay using LinearEasing
1f at delay + delayUnit using LinearEasing
minScale at delay + duration
}), label = ""
)
val scales = arrayListOf<State<Float>>()
for (i in 0 until numberOfDots) {
scales.add(animateScaleWithDelay(delay = i * delayUnit))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
scales.forEach {
Dot(it.value)
Spacer(Modifier.width(spaceBetween))
}
}
}
@Composable
fun DotsFlashing(
modifier: Modifier = Modifier,
dotSize: Dp = 6.dp,
dotColor: Color = Color.Blue,
spaceBetween: Dp = 2.dp,
numberOfDots: Int = 3,
) {
val minAlpha = 0.1f
@Composable
fun Dot(alpha: Float) = Spacer(
Modifier
.size(dotSize)
.alpha(alpha)
.background(
color = dotColor, shape = CircleShape
)
)
val infiniteTransition = rememberInfiniteTransition(label = "")
@Composable
fun animateAlphaWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = minAlpha,
targetValue = minAlpha,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = duration
minAlpha at delay using LinearEasing
1f at delay + delayUnit using LinearEasing
minAlpha at delay + duration
}), label = ""
)
val alphas = arrayListOf<State<Float>>()
for (i in 0 until numberOfDots) {
alphas.add(animateAlphaWithDelay(delay = i * delayUnit))
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
alphas.forEach {
Dot(it.value)
Spacer(Modifier.width(spaceBetween))
}
}
}
@Composable
fun DotsTyping(
modifier: Modifier = Modifier,
dotSize: Dp = 6.dp,
dotColor: Color = Color.Blue,
spaceBetween: Dp = 2.dp,
numberOfDots: Int = 3,
) {
val maxOffset = (numberOfDots * 2).toFloat()
@Composable
fun Dot(offset: Float) {
Spacer(
Modifier
.size(dotSize)
.offset(y = -offset.dp)
.background(
color = dotColor,
shape = CircleShape
)
)
}
val infiniteTransition = rememberInfiniteTransition(label = "")
@Composable
fun animateOffsetWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = duration
0f at delay using LinearEasing
maxOffset at delay + delayUnit using LinearEasing
0f at delay + (duration / 2)
}), label = ""
)
val offsets = arrayListOf<State<Float>>()
for (i in 0 until numberOfDots) {
offsets.add(animateOffsetWithDelay(delay = i * delayUnit))
}
Row(
modifier = modifier.padding(top = maxOffset.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
offsets.forEach {
Dot(it.value)
Spacer(Modifier.width(spaceBetween))
}
}
}
@Composable
fun DotsCollision() {
val maxOffset = 30f
val delayUnit = 500
@Composable
fun Dot(offset: Float) {
Spacer(
Modifier
.size(dotSize)
.offset(x = offset.dp)
.background(
color = dotColor,
shape = CircleShape
)
)
}
val infiniteTransition = rememberInfiniteTransition(label = "")
val offsetLeft by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = delayUnit * 3
0f at 0 using LinearEasing
maxOffset at delayUnit / 2 using LinearEasing
0f at delayUnit
}), label = ""
)
val offsetRight by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = delayUnit * 3
0f at delayUnit using LinearEasing
maxOffset at delayUnit + delayUnit / 2 using LinearEasing
0f at delayUnit * 2
}), label = ""
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = maxOffset.dp)
) {
Dot(offsetLeft)
Spacer(Modifier.width(spaceBetween))
for (i in 0 until numberOfDots - 2) {
Dot(0f)
Spacer(Modifier.width(spaceBetween))
}
Dot(offsetRight)
}
}
@Preview(showBackground = true)
@Composable
fun DotsPreview() {
Column(
modifier = Modifier
.padding(4.dp)
.fillMaxSize()
) {
val spaceSize = 16.dp
Text(
text = "Dots pulsing", //style = MaterialTheme.typography.h5
)
DotsPulsing()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots elastic", //style = MaterialTheme.typography.h5
)
DotsElastic()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots flashing", //style = MaterialTheme.typography.h5
)
DotsFlashing()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots typing", //style = MaterialTheme.typography.h5
)
DotsTyping()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots collision", //style = MaterialTheme.typography.h5
)
DotsCollision()
}
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast.ui
package com.meloda.app.fast.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -11,6 +11,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
@@ -40,3 +41,12 @@ fun ErrorView(
}
}
}
@Preview
@Composable
private fun ErrorViewPreview() {
ErrorView(
text = "Some error occurred",
buttonText = "Restart"
)
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem.components
package com.meloda.app.fast.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -7,6 +7,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun FullScreenLoader(modifier: Modifier = Modifier) {
@@ -19,3 +20,9 @@ fun FullScreenLoader(modifier: Modifier = Modifier) {
CircularProgressIndicator()
}
}
@Preview
@Composable
private fun FullScreenLoaderPreview() {
FullScreenLoader()
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -35,7 +35,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.parseString
import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList
import com.meloda.app.fast.ui.util.ImmutableList
import com.meloda.app.fast.ui.util.ImmutableList.Companion.toImmutableList
import com.meloda.app.fast.ui.util.getString
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -239,7 +241,7 @@ fun MaterialDialog(
}
}
// TODO: 08.04.2023, Danil Nikolaev: refactor this
// TODO: 08.04.2023, Danil Nikolaev: remove this
@Deprecated("need refactoring")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -452,8 +454,9 @@ fun MaterialDialog(
}
}
@Composable
private fun AlertItems(
fun AlertItems(
selectionType: ItemsSelectionType,
items: ImmutableList<DialogItem>,
onItemClick: ((index: Int) -> Unit)? = null,
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem.components
package com.meloda.app.fast.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -8,7 +8,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.meloda.app.fast.designsystem.R
import androidx.compose.ui.tooling.preview.Preview
import com.meloda.app.fast.ui.R
@Composable
fun NoItemsView(
@@ -25,3 +26,11 @@ fun NoItemsView(
)
}
}
@Preview
@Composable
private fun NoItemsViewPreview() {
NoItemsView(
customText = "Nothing here..."
)
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -10,8 +10,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.meloda.app.fast.ui.R
@Preview
@Composable
fun TextFieldErrorText(
modifier: Modifier = Modifier,
@@ -30,3 +30,9 @@ fun TextFieldErrorText(
)
}
}
@Preview
@Composable
private fun TextFieldErrorPreview() {
TextFieldErrorText(text = "Error")
}
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.model
data class TabItem(
val titleResId: Int?,
@@ -1,4 +1,4 @@
package com.meloda.app.fast.datastore.model
package com.meloda.app.fast.ui.model
data class ThemeConfig(
val usingDarkStyle: Boolean,
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.theme
import android.app.Activity
import android.os.Build
@@ -20,8 +20,8 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import com.meloda.app.fast.datastore.model.ThemeConfig
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
import com.meloda.app.fast.ui.R
import com.meloda.app.fast.ui.model.ThemeConfig
import dev.chrisbanes.haze.HazeState
private val googleSansFonts = FontFamily(
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem.colorschemes
package com.meloda.app.fast.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
@@ -1,8 +1,7 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.util
import android.content.res.Configuration
import android.view.KeyEvent
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -17,17 +16,17 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.util.AndroidUtils
import com.meloda.app.fast.datastore.SettingsController
import com.meloda.app.fast.datastore.SettingsKeys
@Composable
fun isUsingDarkTheme(): Boolean {
val nightThemeMode = SettingsController.getInt(
SettingsKeys.KEY_APPEARANCE_DARK_THEME,
SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME
)
val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
// val nightThemeMode = AppCompatDelegate.MODE_NIGHT_YES
// SettingsController.getInt(
// SettingsKeys.KEY_APPEARANCE_DARK_THEME,
// SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME
// )
// val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
// val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
val context = LocalContext.current
@@ -37,7 +36,8 @@ fun isUsingDarkTheme(): Boolean {
val isSystemUsingDarkTheme =
systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
return true
// return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
@Composable
@@ -1,4 +1,4 @@
package com.meloda.app.fast.designsystem
package com.meloda.app.fast.ui.util
import androidx.compose.runtime.Immutable

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 688 B

Some files were not shown because too many files have changed in this diff Show More