Update API version (#147)

* Bump VK Api version to 5.238
* Implemented new authorization flow (at the moment, without auto re-requesting token)
* Add support for sticker pack preview attachments
* Bump LongPoll to version 19
* Improved messages handling
* Fixed coloring issues
* Cache improvements
* Archive screen with full functionality
* Recomposition fixes
* Markdown support for messages bubbles
* Adjust app name font size based on screen width
* Navigation related improvements
* Add logout functionality
This commit is contained in:
2025-04-04 20:43:59 +03:00
committed by GitHub
parent add67b6f8d
commit 89748b72ed
237 changed files with 4896 additions and 3289 deletions
@@ -35,6 +35,7 @@ import org.koin.dsl.bind
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create
import java.util.concurrent.TimeUnit
val networkModule = module {
@@ -44,53 +45,45 @@ val networkModule = module {
single { ChuckerInterceptor.Builder(get()).collector(get()).build() }
singleOf(::VersionInterceptor)
singleOf(::LanguageInterceptor)
single {
OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(get(named("token_interceptor")) as Interceptor)
.addInterceptor(get<VersionInterceptor>())
.addInterceptor(get<LanguageInterceptor>())
.addInterceptor(get<ChuckerInterceptor>())
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(
HttpLoggingInterceptor().apply {
level =
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal]
}
)
.build()
single<OkHttpClient>(named("auth")) {
buildHttpClient(true)
}
single {
Retrofit.Builder()
.baseUrl("${AppConstants.URL_API}/")
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.addConverterFactory(ResponseConverterFactory(get<JsonConverter>()))
.addConverterFactory(MoshiConverterFactory.create(get()))
.client(get())
.build()
single<OkHttpClient> {
buildHttpClient(false)
}
single<Retrofit>(named("auth")) {
buildRetrofit(get(named("auth")))
}
single<Retrofit> {
buildRetrofit(get())
}
singleOf(::OAuthResultCallFactory)
single<Retrofit>(named("oauth")) {
Retrofit.Builder()
.baseUrl("${AppConstants.URL_OAUTH}/")
.addCallAdapterFactory(get<OAuthResultCallFactory>())
.addConverterFactory(MoshiConverterFactory.create(get()))
.client(get())
.client(get(named("auth")))
.build()
}
single<AuthService> {
get<Retrofit>(named("auth")).create()
}
single<OAuthService> {
get<Retrofit>(named("auth")).create()
}
singleOf(::OAuthResultCallFactory)
single { service(AccountService::class.java) }
single { service(AudiosService::class.java) }
single { service(AuthService::class.java) }
single { service(ConversationsService::class.java) }
single { service(FilesService::class.java) }
single { service(LongPollService::class.java) }
single { service(MessagesService::class.java) }
single { service(OAuthService::class.java) }
single { service(PhotosService::class.java) }
single { service(UsersService::class.java) }
single { service(VideosService::class.java) }
@@ -98,3 +91,37 @@ val networkModule = module {
}
private fun <T> Scope.service(className: Class<T>): T = get<Retrofit>().create(className)
private fun Scope.buildHttpClient(forAuth: Boolean): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.apply {
if (!forAuth) {
addInterceptor(get(named("token_interceptor")) as Interceptor)
}
}
.addInterceptor(get<VersionInterceptor>())
.addInterceptor(get<LanguageInterceptor>())
.addInterceptor(get<ChuckerInterceptor>())
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(
HttpLoggingInterceptor().apply {
level =
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal]
}
)
.build()
}
private fun Scope.buildRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("${AppConstants.URL_API}/")
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.addConverterFactory(ResponseConverterFactory(get<JsonConverter>()))
.addConverterFactory(MoshiConverterFactory.create(get()))
.client(client)
.build()
}
@@ -1,16 +1,28 @@
package dev.meloda.fast.network.service.auth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidateLoginResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import dev.meloda.fast.network.ApiResponse
import dev.meloda.fast.network.RestApiError
import com.slack.eithernet.ApiResult
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface AuthService {
@GET(AuthUrls.LOGOUT)
suspend fun logout(
@Query("client_id") clientId: String,
@Query("client_secret") clientSecret: String
): ApiResult<ApiResponse<Int>, RestApiError>
@GET(AuthUrls.VALIDATE_PHONE)
suspend fun validatePhone(
@Query("sid") validationSid: String
@@ -20,4 +32,22 @@ interface AuthService {
suspend fun validateLogin(
@QueryMap param: Map<String, String>
): ApiResult<ApiResponse<ValidateLoginResponse>, RestApiError>
@FormUrlEncoded
@POST(AuthUrls.GET_ANONYM_TOKEN)
suspend fun getAnonymToken(
@FieldMap param: Map<String, String>
): ApiResult<ApiResponse<GetAnonymTokenResponse>, RestApiError>
@FormUrlEncoded
@POST(AuthUrls.EXCHANGE_SILENT_TOKEN)
suspend fun exchangeSilentToken(
@FieldMap param: Map<String, String>
): ApiResult<ApiResponse<ExchangeSilentTokenResponse>, RestApiError>
@FormUrlEncoded
@POST(AuthUrls.GET_EXCHANGE_TOKEN)
suspend fun getExchangeToken(
@FieldMap param: Map<String, String>
): ApiResult<ApiResponse<GetExchangeTokenResponse>, RestApiError>
}
@@ -5,6 +5,12 @@ import dev.meloda.fast.common.AppConstants
object AuthUrls {
private const val URL = AppConstants.URL_API
const val LOGOUT = "$URL/auth.logout"
const val VALIDATE_PHONE = "$URL/auth.validatePhone"
const val VALIDATE_LOGIN = "$URL/auth.validateLogin"
const val GET_ANONYM_TOKEN = "$URL/auth.getAnonymToken"
const val EXCHANGE_SILENT_TOKEN = "$URL/auth.exchangeSilentAuthToken"
const val GET_EXCHANGE_TOKEN = "$URL/auth.getExchangeToken"
}
@@ -32,13 +32,31 @@ interface ConversationsService {
@FormUrlEncoded
@POST(ConversationsUrls.PIN)
suspend fun pin(@FieldMap params: Map<String, String>): ApiResult<ApiResponse<Int>, RestApiError>
suspend fun pin(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.UNPIN)
suspend fun unpin(@FieldMap params: Map<String, String>): ApiResult<ApiResponse<Int>, RestApiError>
suspend fun unpin(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.REORDER_PINNED)
suspend fun reorderPinned(@FieldMap params: Map<String, String>): ApiResult<Unit, RestApiError>
suspend fun reorderPinned(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.ARCHIVE)
suspend fun archive(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.UNARCHIVE)
suspend fun unarchive(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
}
@@ -4,10 +4,14 @@ import dev.meloda.fast.common.AppConstants
object ConversationsUrls {
const val GET = "${AppConstants.URL_API}/messages.getConversations"
const val GET_BY_ID = "${AppConstants.URL_API}/messages.getConversationsById"
const val DELETE = "${AppConstants.URL_API}/messages.deleteConversation"
const val PIN = "${AppConstants.URL_API}/messages.pinConversation"
const val UNPIN = "${AppConstants.URL_API}/messages.unpinConversation"
const val REORDER_PINNED = "${AppConstants.URL_API}/messages.reorderPinnedConversations"
private const val URL = AppConstants.URL_API
const val GET = "$URL/messages.getConversations"
const val GET_BY_ID = "$URL/messages.getConversationsById"
const val DELETE = "$URL/messages.deleteConversation"
const val PIN = "$URL/messages.pinConversation"
const val UNPIN = "$URL/messages.unpinConversation"
const val REORDER_PINNED = "$URL/messages.reorderPinnedConversations"
const val ARCHIVE = "$URL/messages.archiveConversation"
const val UNARCHIVE = "$URL/messages.unarchiveConversation"
}
@@ -20,5 +20,5 @@ interface FriendsService {
@POST(FriendsUrls.GET_ONLINE)
suspend fun getOnlineFriends(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<List<Int>>, RestApiError>
): ApiResult<ApiResponse<List<Long>>, RestApiError>
}
@@ -1,12 +1,15 @@
package dev.meloda.fast.network.service.messages
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.data.VkChatData
import dev.meloda.fast.model.api.data.VkLongPollData
import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.ApiResponse
import dev.meloda.fast.network.RestApiError
import retrofit2.http.FieldMap
@@ -31,7 +34,7 @@ interface MessagesService {
@POST(MessagesUrls.SEND)
suspend fun send(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
): ApiResult<ApiResponse<MessagesSendResponse>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.GET_LONG_POLL_SERVER)
@@ -73,36 +76,35 @@ interface MessagesService {
@POST(MessagesUrls.MARK_AS_IMPORTANT)
suspend fun markAsImportant(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<List<Int>>, RestApiError>
): ApiResult<ApiResponse<List<Long>>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.DELETE)
suspend fun delete(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<List<Any>>, RestApiError>
//
// @FormUrlEncoded
// @POST(MessagesUrls.Edit)
// suspend fun edit(
// @FieldMap params: Map<String, String>
// ): ApiResult<ApiResponse<Int>, RestApiError>
//
//
// @FormUrlEncoded
// @POST(MessagesUrls.GetChat)
// suspend fun getChat(
// @FieldMap params: Map<String, String>
// ): ApiResult<ApiResponse<VkChatData>, RestApiError>
//
// @FormUrlEncoded
// @POST(MessagesUrls.GetConversationMembers)
// suspend fun getConversationMembers(
// @FieldMap params: Map<String, String>
// ): ApiResult<ApiResponse<MessagesGetConversationMembersResponse>, RestApiError>
//
// @FormUrlEncoded
// @POST(MessagesUrls.RemoveChatUser)
// suspend fun removeChatUser(
// @FieldMap params: Map<String, String>
// ): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.EDIT)
suspend fun edit(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.GET_CHAT)
suspend fun getChat(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<VkChatData>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.GET_CONVERSATIONS_MEMBERS)
suspend fun getConversationMembers(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<MessagesGetConversationMembersResponse>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.REMOVE_CHAT_USER)
suspend fun removeChatUser(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
}
@@ -3,21 +3,23 @@ package dev.meloda.fast.network.service.messages
import dev.meloda.fast.common.AppConstants
object MessagesUrls {
const val GET_HISTORY = "${AppConstants.URL_API}/messages.getHistory"
const val SEND = "${AppConstants.URL_API}/messages.send"
const val MARK_AS_IMPORTANT = "${AppConstants.URL_API}/messages.markAsImportant"
const val GET_LONG_POLL_SERVER = "${AppConstants.URL_API}/messages.getLongPollServer"
const val GET_LONG_POLL_HISTORY = "${AppConstants.URL_API}/messages.getLongPollHistory"
const val PIN = "${AppConstants.URL_API}/messages.pin"
const val UNPIN = "${AppConstants.URL_API}/messages.unpin"
const val DELETE = "${AppConstants.URL_API}/messages.delete"
const val EDIT = "${AppConstants.URL_API}/messages.edit"
const val GET_BY_ID = "${AppConstants.URL_API}/messages.getById"
const val MARK_AS_READ = "${AppConstants.URL_API}/messages.markAsRead"
const val GET_CHAT = "${AppConstants.URL_API}/messages.getChat"
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments"
const val CREATE_CHAT = "${AppConstants.URL_API}/messages.createChat"
private const val URL = AppConstants.URL_API
const val GET_HISTORY = "$URL/messages.getHistory"
const val SEND = "$URL/messages.send"
const val MARK_AS_IMPORTANT = "$URL/messages.markAsImportant"
const val GET_LONG_POLL_SERVER = "$URL/messages.getLongPollServer"
const val GET_LONG_POLL_HISTORY = "$URL/messages.getLongPollHistory"
const val PIN = "$URL/messages.pin"
const val UNPIN = "$URL/messages.unpin"
const val DELETE = "$URL/messages.delete"
const val EDIT = "$URL/messages.edit"
const val GET_BY_ID = "$URL/messages.getById"
const val MARK_AS_READ = "$URL/messages.markAsRead"
const val GET_CHAT = "$URL/messages.getChat"
const val GET_CONVERSATIONS_MEMBERS = "$URL/messages.getConversationMembers"
const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser"
const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
const val CREATE_CHAT = "$URL/messages.createChat"
}
@@ -1,23 +1,24 @@
package dev.meloda.fast.network.service.oauth
import dev.meloda.fast.model.api.responses.AuthDirectResponse
import dev.meloda.fast.model.api.responses.GetAnonymousTokenResponse
import com.slack.eithernet.ApiResult
import com.slack.eithernet.DecodeErrorBody
import dev.meloda.fast.model.api.responses.AuthDirectErrorOnlyResponse
import dev.meloda.fast.model.api.responses.AuthDirectResponse
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import retrofit2.http.GET
import retrofit2.http.QueryMap
interface OAuthService {
@DecodeErrorBody
@GET(OAuthUrls.DIRECT_AUTH)
@GET(OAuthUrls.GET_SILENT_TOKEN)
suspend fun auth(
@QueryMap param: Map<String, String>
): ApiResult<AuthDirectResponse, AuthDirectResponse>
): ApiResult<AuthDirectResponse, AuthDirectErrorOnlyResponse>
@DecodeErrorBody
@GET(OAuthUrls.GET_ANONYMOUS_TOKEN)
suspend fun getAnonymousToken(
@GET(OAuthUrls.GET_SILENT_TOKEN)
suspend fun getSilentToken(
@QueryMap param: Map<String, String>
): ApiResult<GetAnonymousTokenResponse, GetAnonymousTokenResponse>
): ApiResult<GetSilentTokenResponse, AuthDirectErrorOnlyResponse>
}
@@ -5,6 +5,5 @@ import dev.meloda.fast.common.AppConstants
object OAuthUrls {
private const val URL = AppConstants.URL_OAUTH
const val DIRECT_AUTH = "$URL/token"
const val GET_ANONYMOUS_TOKEN = "$URL/get_anonym_token"
const val GET_SILENT_TOKEN = "$URL/token"
}