Upstream changes (#23)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
@@ -9,29 +8,22 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".common.AppGlobal"
|
||||
android:allowBackup="false"
|
||||
android:name="com.meloda.app.fast.common.AppGlobal"
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:supportsRtl="false"
|
||||
android:testOnly="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="DataExtractionRules"
|
||||
tools:replace="android:allowBackup"
|
||||
tools:targetApi="tiramisu">
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".screens.main.activity.MainActivity"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -39,10 +31,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".screens.testing.TestActivity" />
|
||||
|
||||
<service
|
||||
android:name=".service.LongPollService"
|
||||
android:name=".service.longpolling.LongPollingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
@@ -53,33 +43,6 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.LongPollQSTileService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_round_settings_24"
|
||||
android:label="Open settings"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.MyCustomControlService"
|
||||
android:exported="true"
|
||||
android:label="Fast"
|
||||
android:permission="android.permission.BIND_CONTROLS">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.controls.ControlsProviderService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
@@ -89,15 +52,6 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.StopLongPollServiceReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.meloda.fast.receiver.ACTION_STOP" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.meloda.app.fast
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.common.extensions.isSdkAtLeast
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import com.meloda.app.fast.datastore.SettingsKeys
|
||||
import com.meloda.app.fast.datastore.UserSettings
|
||||
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||
import com.meloda.app.fast.designsystem.AppTheme
|
||||
import com.meloda.app.fast.designsystem.CheckPermission
|
||||
import com.meloda.app.fast.designsystem.LocalTheme
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.RequestPermission
|
||||
import com.meloda.app.fast.model.MainScreenState
|
||||
import com.meloda.app.fast.service.OnlineService
|
||||
import com.meloda.app.fast.service.longpolling.LongPollingService
|
||||
import org.koin.compose.KoinContext
|
||||
import org.koin.compose.koinInject
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
createNotificationChannels()
|
||||
|
||||
setContent {
|
||||
KoinContext {
|
||||
val userSettings: UserSettings = koinInject()
|
||||
LaunchedEffect(true) {
|
||||
userSettings.updateUsingDarkTheme()
|
||||
}
|
||||
|
||||
val isLongPollInBackground by userSettings.longPollBackground.collectAsStateWithLifecycle()
|
||||
toggleLongPollService(true, isLongPollInBackground)
|
||||
|
||||
val isOnline by userSettings.online.collectAsStateWithLifecycle()
|
||||
LifecycleResumeEffect(isOnline) {
|
||||
toggleOnlineService(isOnline)
|
||||
|
||||
onPauseOrDispose {
|
||||
toggleOnlineService(false)
|
||||
}
|
||||
}
|
||||
|
||||
val theme by userSettings.theme.collectAsStateWithLifecycle()
|
||||
CompositionLocalProvider(
|
||||
LocalTheme provides ThemeConfig(
|
||||
usingDarkStyle = theme.usingDarkStyle,
|
||||
usingDynamicColors = theme.usingDynamicColors,
|
||||
selectedColorScheme = theme.selectedColorScheme,
|
||||
usingAmoledBackground = theme.usingAmoledBackground,
|
||||
usingBlur = theme.usingBlur,
|
||||
multiline = theme.multiline
|
||||
)
|
||||
) {
|
||||
AppTheme(
|
||||
useDarkTheme = LocalTheme.current.usingDarkStyle,
|
||||
useDynamicColors = LocalTheme.current.usingDynamicColors,
|
||||
selectedColorScheme = LocalTheme.current.selectedColorScheme,
|
||||
useAmoledBackground = LocalTheme.current.usingAmoledBackground,
|
||||
) {
|
||||
RootGraph()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
isSdkAtLeast(Build.VERSION_CODES.O) {
|
||||
val dialogsName = "Dialogs"
|
||||
val dialogsDescriptionText = "Channel for dialogs notifications"
|
||||
val dialogsImportance = NotificationManager.IMPORTANCE_HIGH
|
||||
val dialogsChannel =
|
||||
NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply {
|
||||
description = dialogsDescriptionText
|
||||
}
|
||||
|
||||
val longPollName = "Long Polling"
|
||||
val longPollDescriptionText = "Channel for long polling service"
|
||||
val longPollImportance = NotificationManager.IMPORTANCE_NONE
|
||||
val longPollChannel =
|
||||
NotificationChannel("long_polling", longPollName, longPollImportance).apply {
|
||||
description = longPollDescriptionText
|
||||
}
|
||||
|
||||
val notificationManager: NotificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel))
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleLongPollService(
|
||||
enable: Boolean,
|
||||
asForeground: Boolean = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
) {
|
||||
if (enable) {
|
||||
val longPollIntent = Intent(this, LongPollingService::class.java)
|
||||
|
||||
if (asForeground) {
|
||||
ContextCompat.startForegroundService(this, longPollIntent)
|
||||
} else {
|
||||
startService(longPollIntent)
|
||||
}
|
||||
} else {
|
||||
stopService(Intent(this, LongPollingService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleOnlineService(enable: Boolean) {
|
||||
if (enable) {
|
||||
startService(Intent(this, OnlineService::class.java))
|
||||
} else {
|
||||
stopService(Intent(this, OnlineService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServices() {
|
||||
toggleOnlineService(enable = false)
|
||||
|
||||
val asForeground = SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
|
||||
if (!asForeground) {
|
||||
toggleLongPollService(enable = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNewLanguage(newLanguage: String) {
|
||||
val appLocales = AppCompatDelegate.getApplicationLocales()
|
||||
if (newLanguage.isEmpty()) {
|
||||
if (!appLocales.isEmpty) {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||
}
|
||||
} else {
|
||||
if (!appLocales.toLanguageTags().startsWith(newLanguage)) {
|
||||
val newLocale = LocaleListCompat.forLanguageTags(newLanguage)
|
||||
AppCompatDelegate.setApplicationLocales(newLocale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopServices()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun NotificationsPermissionChecker(
|
||||
screenState: MainScreenState,
|
||||
viewModel: MainViewModel
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||
|
||||
val permission =
|
||||
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
if (screenState.isNeedToOpenAppPermissions) {
|
||||
viewModel.onAppPermissionsOpened()
|
||||
|
||||
LocalContext.current.apply {
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", this.packageName, null)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.isNeedToRequestNotifications) {
|
||||
RequestPermission(permission = permission)
|
||||
}
|
||||
|
||||
val isNeedToCheckNotificationsPermission by remember {
|
||||
derivedStateOf {
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (isNeedToCheckNotificationsPermission) {
|
||||
CheckPermission(
|
||||
showRationale = {
|
||||
MaterialDialog(
|
||||
title = UiText.Resource(UiR.string.warning),
|
||||
text = UiText.Simple("The application will not be able to work properly without permission to send notifications."),
|
||||
confirmText = UiText.Simple("Grant"),
|
||||
confirmAction = {
|
||||
viewModel.onRequestNotificationsPermissionClicked(true)
|
||||
},
|
||||
cancelText = UiText.Resource(UiR.string.cancel),
|
||||
cancelAction = viewModel::onNotificationsAlertNegativeClicked,
|
||||
onDismissAction = viewModel::onNotificationsAlertNegativeClicked,
|
||||
buttonsInvokeDismiss = false
|
||||
)
|
||||
},
|
||||
onDenied = {
|
||||
MaterialDialog(
|
||||
title = UiText.Resource(UiR.string.warning),
|
||||
text = UiText.Simple("The application needs permission to send notifications to update messages and other information."),
|
||||
confirmText = UiText.Simple("Grant"),
|
||||
confirmAction = {
|
||||
viewModel.onRequestNotificationsPermissionClicked(false)
|
||||
},
|
||||
cancelText = UiText.Resource(UiR.string.cancel),
|
||||
onDismissAction = {},
|
||||
buttonsInvokeDismiss = false
|
||||
)
|
||||
},
|
||||
permission = permission
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.meloda.app.fast
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.meloda.app.fast.conversations.navigation.Conversations
|
||||
import com.meloda.app.fast.conversations.navigation.conversationsRoute
|
||||
import com.meloda.app.fast.friends.navigation.Friends
|
||||
import com.meloda.app.fast.friends.navigation.friendsRoute
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@Serializable
|
||||
object MainGraph
|
||||
|
||||
@Serializable
|
||||
object Main
|
||||
|
||||
@Serializable
|
||||
object Profile
|
||||
|
||||
data class BottomNavigationItem(
|
||||
val titleResId: Int,
|
||||
val selectedIconResId: Int,
|
||||
val unselectedIconResId: Int,
|
||||
val route: Any,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun NavGraphBuilder.mainScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
onNavigateToMessagesHistory: (conversationId: Int) -> Unit,
|
||||
) {
|
||||
val items = listOf(
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_friends,
|
||||
selectedIconResId = UiR.drawable.baseline_people_alt_24,
|
||||
unselectedIconResId = UiR.drawable.outline_people_alt_24,
|
||||
route = Friends,
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_conversations,
|
||||
selectedIconResId = UiR.drawable.baseline_chat_24,
|
||||
unselectedIconResId = UiR.drawable.outline_chat_24,
|
||||
route = Conversations
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_profile,
|
||||
selectedIconResId = UiR.drawable.baseline_account_circle_24,
|
||||
unselectedIconResId = UiR.drawable.outline_account_circle_24,
|
||||
route = Profile
|
||||
)
|
||||
)
|
||||
val routes = items.map(BottomNavigationItem::route)
|
||||
|
||||
composable<Main> {
|
||||
val navController = rememberNavController()
|
||||
|
||||
var selectedItemIndex by rememberSaveable {
|
||||
mutableIntStateOf(1)
|
||||
}
|
||||
|
||||
var isBottomBarVisible by rememberSaveable {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
AnimatedVisibility(
|
||||
visible = isBottomBarVisible,
|
||||
enter = slideIn { IntOffset(0, 400) },
|
||||
exit = slideOut { IntOffset(0, 400) }
|
||||
) {
|
||||
NavigationBar {
|
||||
items.forEachIndexed { index, item ->
|
||||
NavigationBarItem(
|
||||
selected = selectedItemIndex == index,
|
||||
onClick = {
|
||||
if (selectedItemIndex != index) {
|
||||
val currentRoute = routes[selectedItemIndex]
|
||||
|
||||
selectedItemIndex = index
|
||||
navController.navigate(item.route) {
|
||||
popUpTo(route = currentRoute) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (selectedItemIndex == index) item.selectedIconResId
|
||||
else item.unselectedIconResId
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = padding.calculateBottomPadding())
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = MainGraph,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
navigation<MainGraph>(startDestination = Conversations) {
|
||||
friendsRoute(
|
||||
onError = onError,
|
||||
navController = navController
|
||||
)
|
||||
conversationsRoute(
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
navController = navController,
|
||||
onListScrollingUp = { isScrolling ->
|
||||
// isBottomBarVisible = isScrolling
|
||||
}
|
||||
)
|
||||
|
||||
composable<Profile> {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(id = UiR.string.title_profile))
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onNavigateToSettings) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.meloda.app.fast
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.app.fast.common.UserConfig
|
||||
import com.meloda.app.fast.common.extensions.setValue
|
||||
import com.meloda.app.fast.common.extensions.updateValue
|
||||
import com.meloda.app.fast.data.db.AccountsRepository
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import com.meloda.app.fast.datastore.SettingsKeys
|
||||
import com.meloda.app.fast.datastore.UserSettings
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.app.fast.model.LongPollState
|
||||
import com.meloda.app.fast.model.MainScreenState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface MainViewModel {
|
||||
|
||||
val screenState: StateFlow<MainScreenState>
|
||||
|
||||
val longPollState: StateFlow<LongPollState>
|
||||
val startOnlineService: StateFlow<Boolean>
|
||||
|
||||
fun useDynamicColorsChanged(use: Boolean)
|
||||
|
||||
fun useDarkThemeChanged(use: Boolean)
|
||||
|
||||
fun onRequestNotificationsPermissionClicked(fromRationale: Boolean)
|
||||
fun onNotificationsAlertNegativeClicked()
|
||||
|
||||
fun onNotificationsRequested()
|
||||
|
||||
fun onAppPermissionsOpened()
|
||||
|
||||
fun onError(error: BaseError)
|
||||
|
||||
fun onAuthOpened()
|
||||
}
|
||||
|
||||
class MainViewModelImpl(
|
||||
private val accountsRepository: AccountsRepository,
|
||||
private val userSettings: UserSettings
|
||||
) : MainViewModel, ViewModel() {
|
||||
|
||||
init {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
override val screenState = MutableStateFlow(MainScreenState.EMPTY)
|
||||
|
||||
override val longPollState = MutableStateFlow(
|
||||
if (SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
) {
|
||||
LongPollState.ForegroundService
|
||||
} else {
|
||||
LongPollState.DefaultService
|
||||
}
|
||||
)
|
||||
override val startOnlineService = MutableStateFlow(
|
||||
SettingsController.getBoolean(
|
||||
SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||
SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||
)
|
||||
)
|
||||
|
||||
override fun useDynamicColorsChanged(use: Boolean) {
|
||||
screenState.updateValue(screenState.value.copy(useDynamicColors = use))
|
||||
}
|
||||
|
||||
override fun useDarkThemeChanged(use: Boolean) {
|
||||
screenState.updateValue(screenState.value.copy(useDarkTheme = use))
|
||||
}
|
||||
|
||||
override fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) {
|
||||
screenState.setValue { old ->
|
||||
if (fromRationale) {
|
||||
old.copy(isNeedToOpenAppPermissions = true)
|
||||
} else {
|
||||
old.copy(isNeedToRequestNotifications = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotificationsAlertNegativeClicked() {
|
||||
SettingsController.edit {
|
||||
putBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
false
|
||||
)
|
||||
}
|
||||
userSettings.setLongPollBackground(false)
|
||||
}
|
||||
|
||||
override fun onNotificationsRequested() {
|
||||
screenState.setValue { old -> old.copy(isNeedToRequestNotifications = false) }
|
||||
}
|
||||
|
||||
override fun onAppPermissionsOpened() {
|
||||
screenState.setValue { old -> old.copy(isNeedToOpenAppPermissions = false) }
|
||||
}
|
||||
|
||||
override fun onError(error: BaseError) {
|
||||
when (error) {
|
||||
BaseError.SessionExpired -> {
|
||||
screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthOpened() {
|
||||
screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) }
|
||||
}
|
||||
|
||||
private fun loadAccounts() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val accounts = accountsRepository.getAccounts()
|
||||
|
||||
Log.d("MainViewModel", "initUserConfig: accounts: $accounts")
|
||||
|
||||
if (accounts.isNotEmpty()) {
|
||||
val currentAccount = accounts.find { it.userId == UserConfig.currentUserId }
|
||||
if (currentAccount != null) {
|
||||
UserConfig.apply {
|
||||
this.userId = currentAccount.userId
|
||||
this.accessToken = currentAccount.accessToken
|
||||
this.fastToken = currentAccount.fastToken
|
||||
this.trustedHash = currentAccount.trustedHash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
accounts = accounts,
|
||||
accountsLoaded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.meloda.app.fast
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.meloda.app.fast.auth.AuthGraph
|
||||
import com.meloda.app.fast.auth.authNavGraph
|
||||
import com.meloda.app.fast.auth.navigateToAuth
|
||||
import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsRoute
|
||||
import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials
|
||||
import com.meloda.app.fast.common.UserConfig
|
||||
import com.meloda.app.fast.languagepicker.navigation.languagePickerRoute
|
||||
import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker
|
||||
import com.meloda.app.fast.messageshistory.navigation.messagesHistoryRoute
|
||||
import com.meloda.app.fast.messageshistory.navigation.navigateToMessagesHistory
|
||||
import com.meloda.app.fast.settings.presentation.navigateToSettings
|
||||
import com.meloda.app.fast.settings.presentation.settingsRoute
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun RootGraph(navController: NavHostController = rememberNavController()) {
|
||||
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
if (screenState.isNeedToOpenAuth) {
|
||||
viewModel.onAuthOpened()
|
||||
navController.navigateToAuth(clearBackStack = true)
|
||||
}
|
||||
|
||||
if (screenState.accountsLoaded) {
|
||||
val isNeedToShowConversations by remember {
|
||||
derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() }
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = if (isNeedToShowConversations) Main else AuthGraph,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
authNavGraph(
|
||||
onError = viewModel::onError,
|
||||
onNavigateToMain = navController::navigateToMain,
|
||||
navController = navController
|
||||
)
|
||||
mainScreen(
|
||||
onError = viewModel::onError,
|
||||
onNavigateToSettings = navController::navigateToSettings,
|
||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory
|
||||
)
|
||||
|
||||
messagesHistoryRoute(
|
||||
onError = viewModel::onError,
|
||||
onBack = navController::navigateUp,
|
||||
onNavigateToChatAttachments = navController::navigateToChatMaterials
|
||||
)
|
||||
chatMaterialsRoute(
|
||||
onBack = navController::navigateUp
|
||||
)
|
||||
|
||||
settingsRoute(
|
||||
onError = viewModel::onError,
|
||||
onBack = navController::navigateUp,
|
||||
onNavigateToAuth = { navController.navigateToAuth(true) },
|
||||
onNavigateToLanguagePicker = navController::navigateToLanguagePicker
|
||||
)
|
||||
languagePickerRoute(onBack = navController::navigateUp)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationsPermissionChecker(
|
||||
screenState = screenState,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
fun NavController.navigateToMain() {
|
||||
this.navigate(Main) {
|
||||
popUpTo(0) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.meloda.app.fast.common
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.meloda.app.fast.common.di.applicationModule
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.GlobalContext.startKoin
|
||||
|
||||
class AppGlobal : Application(), ImageLoaderFactory {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
SettingsController.init(preferences)
|
||||
UserConfig.init(preferences)
|
||||
|
||||
initKoin()
|
||||
}
|
||||
|
||||
private fun initKoin() {
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(this@AppGlobal)
|
||||
modules(applicationModule)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader = get()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.meloda.app.fast.common.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.PowerManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.meloda.app.fast.MainViewModelImpl
|
||||
import com.meloda.app.fast.auth.authModule
|
||||
import com.meloda.app.fast.conversations.di.conversationsModule
|
||||
import com.meloda.app.fast.data.di.dataModule
|
||||
import com.meloda.app.fast.friends.di.friendsModule
|
||||
import com.meloda.app.fast.languagepicker.di.languagePickerModule
|
||||
import com.meloda.app.fast.messageshistory.di.messagesHistoryModule
|
||||
import com.meloda.app.fast.photoviewer.di.photoViewModule
|
||||
import com.meloda.app.fast.profile.di.profileModule
|
||||
import com.meloda.app.fast.service.longpolling.di.longPollModule
|
||||
import com.meloda.app.fast.settings.di.settingsModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.module
|
||||
|
||||
val applicationModule = module {
|
||||
includes(dataModule)
|
||||
includes(
|
||||
authModule,
|
||||
conversationsModule,
|
||||
settingsModule,
|
||||
messagesHistoryModule,
|
||||
photoViewModule,
|
||||
languagePickerModule,
|
||||
longPollModule,
|
||||
friendsModule,
|
||||
profileModule
|
||||
)
|
||||
|
||||
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
||||
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
|
||||
singleOf(PreferenceManager::getDefaultSharedPreferences)
|
||||
single<Resources> { androidContext().resources }
|
||||
factory<PowerManager> { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||
|
||||
viewModelOf(::MainViewModelImpl) {
|
||||
qualifier = qualifier("main")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.meloda.app.fast.model
|
||||
|
||||
sealed class LongPollState {
|
||||
data object ForegroundService : LongPollState()
|
||||
data object DefaultService : LongPollState()
|
||||
data object Stop : LongPollState()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.meloda.app.fast.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
|
||||
@Immutable
|
||||
data class MainScreenState(
|
||||
val accounts: List<AccountEntity>,
|
||||
val accountsLoaded: Boolean,
|
||||
val useDarkTheme: Boolean,
|
||||
val useDynamicColors: Boolean,
|
||||
val isNeedToRequestNotifications: Boolean,
|
||||
val isNeedToOpenAppPermissions: Boolean,
|
||||
val isNeedToOpenAuth: Boolean,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: MainScreenState = MainScreenState(
|
||||
accounts = emptyList(),
|
||||
accountsLoaded = false,
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
useDarkTheme = false,
|
||||
useDynamicColors = false,
|
||||
isNeedToRequestNotifications = false,
|
||||
isNeedToOpenAppPermissions = false,
|
||||
isNeedToOpenAuth = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.meloda.app.fast.model
|
||||
|
||||
sealed class ServicesState {
|
||||
data object Started : ServicesState()
|
||||
data object Stopped : ServicesState()
|
||||
data object Unknown : ServicesState()
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.meloda.fast.receiver
|
||||
package com.meloda.app.fast.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -12,4 +12,4 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
onReceiveAction?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.meloda.app.fast.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.meloda.app.fast.common.UserConfig
|
||||
import com.meloda.app.fast.common.extensions.createTimerFlow
|
||||
import com.meloda.app.fast.data.api.account.AccountUseCase
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class OnlineService : Service() {
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d(TAG, "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
private val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||
|
||||
private val useCase: AccountUseCase by inject()
|
||||
|
||||
private var timerJob: Job? = null
|
||||
private var onlineJob: Job? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
Log.d(STATE_TAG, "onBind: intent: $intent")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (startId > 1) return START_STICKY
|
||||
|
||||
Log.d(STATE_TAG, "onStartCommand: flags: $flags; startId: $startId\ninstance: $this")
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
// if (AppGlobal.preferences.getBoolean(
|
||||
// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||
// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||
// )
|
||||
// ) {
|
||||
// createTimer()
|
||||
// }
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun createTimer() {
|
||||
timerJob = createTimerFlow(
|
||||
isNeedToEndCondition = { false },
|
||||
onStartAction = ::setOnline,
|
||||
onTickAction = ::setOnline,
|
||||
interval = 5.minutes
|
||||
).launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun setOnline() {
|
||||
if (onlineJob != null) return
|
||||
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
// if (!AppGlobal.preferences.getBoolean(
|
||||
// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS,
|
||||
// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS
|
||||
// )
|
||||
// ) return
|
||||
|
||||
Log.d(TAG, "setOnline()")
|
||||
|
||||
onlineJob = coroutineScope.launch {
|
||||
val token = UserConfig.fastToken ?: UserConfig.accessToken
|
||||
|
||||
if (token.isBlank()) {
|
||||
Log.d(TAG, "setOnline: token is empty")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val response = useCase.setOnline(
|
||||
voip = false,
|
||||
accessToken = token
|
||||
)
|
||||
Log.d(TAG, "setOnline: response: $response")
|
||||
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
|
||||
}
|
||||
|
||||
private suspend fun setOffline() {
|
||||
Log.d(TAG, "setOffline()")
|
||||
|
||||
val response = useCase.setOffline(
|
||||
accessToken = UserConfig.accessToken
|
||||
)
|
||||
|
||||
Log.d(TAG, "setOffline: response: $response")
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.d(STATE_TAG, "onLowMemory")
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(STATE_TAG, "onDestroy")
|
||||
|
||||
timerJob?.cancel("OnlineService destroyed")
|
||||
onlineJob?.cancel("OnlineService destroyed")
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "OnlineService"
|
||||
private const val STATE_TAG = "OnlineServiceState"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package com.meloda.app.fast.service.longpolling
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.conena.nanokt.android.app.stopForegroundCompat
|
||||
import com.meloda.app.fast.common.UserConfig
|
||||
import com.meloda.app.fast.common.VkConstants
|
||||
import com.meloda.app.fast.common.extensions.listenValue
|
||||
import com.meloda.app.fast.data.LongPollUpdatesParser
|
||||
import com.meloda.app.fast.data.LongPollUseCase
|
||||
import com.meloda.app.fast.data.processState
|
||||
import com.meloda.app.fast.datastore.SettingsController
|
||||
import com.meloda.app.fast.datastore.SettingsKeys
|
||||
import com.meloda.app.fast.model.api.data.LongPollUpdates
|
||||
import com.meloda.app.fast.model.api.data.VkLongPollData
|
||||
import com.meloda.app.fast.util.NotificationsUtils
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class LongPollingService : Service() {
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e(TAG, "error: $throwable")
|
||||
|
||||
if (throwable !is NoAccessTokenException) {
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.IO + job + exceptionHandler
|
||||
|
||||
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||
|
||||
private val longPollUseCase: LongPollUseCase by inject()
|
||||
private val updatesParser: LongPollUpdatesParser by inject()
|
||||
private val preferences: SharedPreferences by inject()
|
||||
|
||||
private var currentJob: Job? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(STATE_TAG, "onCreate()")
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
Log.d(STATE_TAG, "onBind: intent: $intent")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (startId > 1) return START_STICKY
|
||||
|
||||
val asForeground = preferences.getBoolean(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
|
||||
Log.d(
|
||||
STATE_TAG,
|
||||
"onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this"
|
||||
)
|
||||
|
||||
if (currentJob != null) {
|
||||
currentJob?.cancel()
|
||||
currentJob = null
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
currentJob = startPolling().also { it.join() }
|
||||
}
|
||||
|
||||
val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
.putExtra(Settings.EXTRA_CHANNEL_ID, "long_polling")
|
||||
} else {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", packageName, null))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val openCategorySettingsPendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
1,
|
||||
openCategorySettingsIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
if (asForeground) {
|
||||
val notification =
|
||||
NotificationsUtils.createNotification(
|
||||
context = this,
|
||||
title = "LongPoll",
|
||||
contentText = "нажмите, чтобы убрать уведомление",
|
||||
notRemovable = false,
|
||||
channelId = "long_polling",
|
||||
priority = NotificationsUtils.NotificationPriority.Low,
|
||||
category = NotificationCompat.CATEGORY_SERVICE,
|
||||
customNotificationId = NOTIFICATION_ID,
|
||||
contentIntent = openCategorySettingsPendingIntent
|
||||
).build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
} else {
|
||||
stopForegroundCompat(ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun startPolling(): Job {
|
||||
if (job.isCompleted || job.isCancelled) {
|
||||
Log.d(STATE_TAG, "job is completed or cancelled")
|
||||
throw Exception("Job is over")
|
||||
}
|
||||
|
||||
Log.d(STATE_TAG, "job started")
|
||||
|
||||
return coroutineScope.launch {
|
||||
if (UserConfig.accessToken.isEmpty()) {
|
||||
throw NoAccessTokenException
|
||||
}
|
||||
|
||||
var serverInfo = getServerInfo()
|
||||
?: throw LongPollException(message = "bad VK response (server info)")
|
||||
|
||||
var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo)
|
||||
?: throw LongPollException(message = "initiation error: bad VK response (last updates)")
|
||||
|
||||
var failCount = 0
|
||||
|
||||
while (job.isActive) {
|
||||
if (lastUpdatesResponse == null) {
|
||||
failCount++
|
||||
serverInfo = getServerInfo()
|
||||
?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)")
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
when (lastUpdatesResponse.failed) {
|
||||
1 -> {
|
||||
val newTs = lastUpdatesResponse.ts ?: kotlin.run {
|
||||
failCount++
|
||||
serverInfo.ts
|
||||
}
|
||||
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
|
||||
}
|
||||
|
||||
2, 3 -> {
|
||||
serverInfo = getServerInfo()
|
||||
?: throw LongPollException(
|
||||
message = "failed retrieving server info after error: bad VK response (server info #3)"
|
||||
)
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val newTs = lastUpdatesResponse.ts
|
||||
|
||||
if (newTs == null) {
|
||||
failCount++
|
||||
} else {
|
||||
val updates = lastUpdatesResponse.updates
|
||||
|
||||
if (updates == null) {
|
||||
failCount++
|
||||
} else {
|
||||
updates.forEach(updatesParser::parseNextUpdate)
|
||||
}
|
||||
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getServerInfo(): VkLongPollData? = suspendCoroutine {
|
||||
longPollUseCase.getLongPollServer(
|
||||
needPts = true,
|
||||
version = VkConstants.LP_VERSION
|
||||
).listenValue(coroutineScope) { state ->
|
||||
state.processState(
|
||||
success = { response ->
|
||||
Log.d(TAG, "getServerInfo: serverInfoResponse: $response")
|
||||
it.resume(response)
|
||||
},
|
||||
error = { error ->
|
||||
Log.e(TAG, "getServerInfo: $error")
|
||||
it.resume(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUpdatesResponse(
|
||||
server: VkLongPollData
|
||||
): LongPollUpdates? = suspendCoroutine {
|
||||
longPollUseCase.getLongPollUpdates(
|
||||
serverUrl = "https://${server.server}",
|
||||
key = server.key,
|
||||
ts = server.ts,
|
||||
wait = 25,
|
||||
mode = 2 or 8 or 32 or 64 or 128,
|
||||
version = VkConstants.LP_VERSION
|
||||
).listenValue(coroutineScope) { state ->
|
||||
state.processState(
|
||||
success = { response ->
|
||||
Log.d(TAG, "lastUpdateResponse: $response")
|
||||
it.resume(response)
|
||||
},
|
||||
error = { error ->
|
||||
Log.d(TAG, "getUpdatesResponse: error: $error")
|
||||
it.resume(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(STATE_TAG, "onDestroy")
|
||||
try {
|
||||
SettingsController.edit {
|
||||
putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true)
|
||||
}
|
||||
job.cancel()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.d(STATE_TAG, "onLowMemory")
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "LongPollTask"
|
||||
|
||||
private const val STATE_TAG = "LongPollServiceState"
|
||||
|
||||
const val KEY_LONG_POLL_WAS_DESTROYED = "long_poll_was_destroyed"
|
||||
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
}
|
||||
}
|
||||
|
||||
private data class LongPollException(override val message: String) : Throwable()
|
||||
private data object NoAccessTokenException : Throwable()
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.meloda.app.fast.service.longpolling.di
|
||||
|
||||
import com.meloda.app.fast.data.LongPollUpdatesParser
|
||||
import com.meloda.app.fast.data.LongPollUseCase
|
||||
import com.meloda.app.fast.data.LongPollUseCaseImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val longPollModule = module {
|
||||
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
|
||||
singleOf(::LongPollUpdatesParser)
|
||||
}
|
||||
+3
-4
@@ -1,11 +1,11 @@
|
||||
package com.meloda.fast.util
|
||||
package com.meloda.app.fast.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
object NotificationsUtils {
|
||||
|
||||
@@ -27,7 +27,7 @@ object NotificationsUtils {
|
||||
actions: List<NotificationCompat.Action> = emptyList(),
|
||||
): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_fast_logo)
|
||||
.setSmallIcon(UiR.drawable.ic_fast_logo)
|
||||
.setContentTitle(title)
|
||||
.setPriority(priority.value)
|
||||
.setContentIntent(contentIntent)
|
||||
@@ -69,5 +69,4 @@ object NotificationsUtils {
|
||||
enum class NotificationPriority(val value: Int) {
|
||||
Default(0), Low(-1), Min(-2), High(1), Max(2)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
enum class ApiEvent(val value: Int) {
|
||||
MessageSetFlags(2),
|
||||
MessageClearFlags(3),
|
||||
MessageNew(4),
|
||||
MessageEdit(5),
|
||||
MessageReadIncoming(6),
|
||||
MessageReadOutgoing(7),
|
||||
MessagesDeleted(13),
|
||||
PinUnpinConversation(20),
|
||||
PrivateTyping(61),
|
||||
ChatTyping(62),
|
||||
OneMoreTyping(63),
|
||||
VoiceRecording(64),
|
||||
PhotoUploading(65),
|
||||
VideoUploading(66),
|
||||
FileUploading(67),
|
||||
UnreadCountUpdate(80)
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: Int) = values().firstOrNull { it.value == value }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
object ApiExtensions {
|
||||
|
||||
val Boolean.intString get() = (if (this) 1 else 0).toString()
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.model.AppAccount
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
object UserConfig {
|
||||
|
||||
private const val ARG_CURRENT_USER_ID = "current_user_id"
|
||||
|
||||
const val FAST_APP_ID = "6964679"
|
||||
|
||||
private val preferences get() = AppGlobal.preferences
|
||||
|
||||
var currentUserId: Int = -1
|
||||
get() = preferences.getInt(ARG_CURRENT_USER_ID, -1)
|
||||
set(value) {
|
||||
field = value
|
||||
preferences.edit { putInt(ARG_CURRENT_USER_ID, value) }
|
||||
}
|
||||
|
||||
var userId: Int = -1
|
||||
var accessToken: String = ""
|
||||
var fastToken: String? = ""
|
||||
|
||||
fun parse(account: AppAccount) {
|
||||
this.userId = account.userId
|
||||
this.accessToken = account.accessToken
|
||||
this.fastToken = account.fastToken
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
currentUserId = -1
|
||||
accessToken = ""
|
||||
fastToken = ""
|
||||
userId = -1
|
||||
}
|
||||
|
||||
fun isLoggedIn(): Boolean {
|
||||
return currentUserId > 0 && userId > 0 && accessToken.isNotBlank()
|
||||
}
|
||||
|
||||
val vkUser: MutableStateFlow<VkUser?> = MutableStateFlow(null)
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import com.meloda.fast.api.model.attachments.*
|
||||
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
object VKConstants {
|
||||
|
||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||
|
||||
const val USER_FIELDS =
|
||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate"
|
||||
|
||||
const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
|
||||
|
||||
const val API_VERSION = "5.173"
|
||||
const val LP_VERSION = 10
|
||||
|
||||
const val VK_APP_ID = "2274003"
|
||||
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||
|
||||
const val FAST_GROUP_ID = -119516304
|
||||
const val FAST_APP_ID = "6964679"
|
||||
|
||||
object Auth {
|
||||
const val SCOPE = "notify," +
|
||||
"friends," +
|
||||
"photos," +
|
||||
"audio," +
|
||||
"video," +
|
||||
"docs," +
|
||||
"status," +
|
||||
"notes," +
|
||||
"pages," +
|
||||
"wall," +
|
||||
"groups," +
|
||||
"messages," +
|
||||
"offline," +
|
||||
"notifications"
|
||||
|
||||
object GrantType {
|
||||
const val PASSWORD = "password"
|
||||
}
|
||||
}
|
||||
|
||||
val restrictedToEditAttachments = listOf<Class<out VkAttachment>>(
|
||||
VkCall::class.java,
|
||||
VkCurator::class.java,
|
||||
VkEvent::class.java,
|
||||
VkGift::class.java,
|
||||
VkGraffiti::class.java,
|
||||
VkGroupCall::class.java,
|
||||
VkStory::class.java,
|
||||
VkVoiceMessage::class.java,
|
||||
VkWidget::class.java
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
package com.meloda.fast.api.base
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import okio.IOException
|
||||
|
||||
open class ApiError(
|
||||
@SerializedName("error", alternate = ["error_code"])
|
||||
val error: String? = null,
|
||||
@SerializedName("error_msg", alternate = ["error_description"])
|
||||
open val errorMessage: String? = null,
|
||||
@SerializedName("error_type")
|
||||
val errorType: String? = null,
|
||||
val throwable: Throwable? = null
|
||||
) : IOException() {
|
||||
|
||||
override fun toString(): String {
|
||||
return Gson().toJson(this)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.meloda.fast.api.base
|
||||
|
||||
data class ApiResponse<T>(
|
||||
val error: ApiError? = null,
|
||||
val response: T? = null
|
||||
) {
|
||||
val isSuccessful get() = error == null && response != null
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.meloda.fast.api.base
|
||||
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import okio.IOException
|
||||
|
||||
class AttachmentClassNameIsEmptyException(attachment: VkAttachment) :
|
||||
IOException(
|
||||
"attachment ${attachment.javaClass.name} does not have declared field \"className\""
|
||||
)
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.meloda.fast.api.longpoll
|
||||
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
|
||||
sealed class LongPollEvent {
|
||||
|
||||
data class VkMessageNewEvent(
|
||||
val message: VkMessage,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>,
|
||||
) : LongPollEvent()
|
||||
|
||||
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
|
||||
|
||||
data class VkMessageReadIncomingEvent(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val unreadCount: Int,
|
||||
) : LongPollEvent()
|
||||
|
||||
data class VkMessageReadOutgoingEvent(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val unreadCount: Int,
|
||||
) : LongPollEvent()
|
||||
|
||||
data class VkConversationPinStateChangedEvent(
|
||||
val peerId: Int,
|
||||
val majorId: Int,
|
||||
) : LongPollEvent()
|
||||
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
package com.meloda.fast.api.longpoll
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonArray
|
||||
import com.meloda.fast.api.ApiEvent
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.network.ApiAnswer
|
||||
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
|
||||
import com.meloda.fast.base.viewmodel.VkEventCallback
|
||||
import com.meloda.fast.data.messages.MessagesRepository
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate")
|
||||
class LongPollUpdatesParser(private val messagesRepository: MessagesRepository) : CoroutineScope {
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d("LongPollUpdatesParser", "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
|
||||
mutableMapOf()
|
||||
|
||||
fun parseNextUpdate(event: JsonArray) {
|
||||
val eventId = event[0].asInt
|
||||
val eventType: ApiEvent? = ApiEvent.parse(eventId)
|
||||
|
||||
if (eventType == null) {
|
||||
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
||||
return
|
||||
}
|
||||
|
||||
when (eventType) {
|
||||
ApiEvent.MessageSetFlags -> parseMessageSetFlags(eventType, event)
|
||||
ApiEvent.MessageClearFlags -> parseMessageClearFlags(eventType, event)
|
||||
ApiEvent.MessageNew -> parseMessageNew(eventType, event)
|
||||
ApiEvent.MessageEdit -> parseMessageEdit(eventType, event)
|
||||
ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event)
|
||||
ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event)
|
||||
ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event)
|
||||
ApiEvent.PinUnpinConversation -> parseConversationPinStateChanged(eventType, event)
|
||||
ApiEvent.PrivateTyping -> onNewEvent(eventType, event)
|
||||
ApiEvent.ChatTyping -> onNewEvent(eventType, event)
|
||||
ApiEvent.OneMoreTyping -> onNewEvent(eventType, event)
|
||||
ApiEvent.VoiceRecording -> onNewEvent(eventType, event)
|
||||
ApiEvent.PhotoUploading -> onNewEvent(eventType, event)
|
||||
ApiEvent.VideoUploading -> onNewEvent(eventType, event)
|
||||
ApiEvent.FileUploading -> onNewEvent(eventType, event)
|
||||
ApiEvent.UnreadCountUpdate -> onNewEvent(eventType, event)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun onNewEvent(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val peerId = event[1].asInt
|
||||
val majorId = event[2].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.PinUnpinConversation]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkConversationPinStateChangedEvent(
|
||||
peerId = peerId,
|
||||
majorId = majorId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val messageId = event[1].asInt
|
||||
|
||||
launch {
|
||||
val newMessageEvent: LongPollEvent.VkMessageNewEvent =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
listenersMap[ApiEvent.MessageNew]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
|
||||
.onEvent(newMessageEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val messageId = event[1].asInt
|
||||
|
||||
launch {
|
||||
val editedMessageEvent: LongPollEvent.VkMessageEditEvent =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
listenersMap[ApiEvent.MessageEdit]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
|
||||
.onEvent(editedMessageEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val peerId = event[1].asInt
|
||||
val messageId = event[2].asInt
|
||||
val unreadCount = event[3].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadIncomingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val peerId = event[1].asInt
|
||||
val messageId = event[2].asInt
|
||||
val unreadCount = event[3].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
|
||||
coroutineScope {
|
||||
suspendCoroutine {
|
||||
launch {
|
||||
val normalMessageResponse = messagesRepository.getById(
|
||||
MessagesGetByIdRequest(
|
||||
messagesIds = listOf(messageId),
|
||||
extended = true,
|
||||
fields = VKConstants.ALL_FIELDS
|
||||
)
|
||||
)
|
||||
|
||||
if (normalMessageResponse.isError()) {
|
||||
normalMessageResponse.error.throwable?.run { throw this }
|
||||
}
|
||||
|
||||
val messagesResponse =
|
||||
(normalMessageResponse as? ApiAnswer.Success)?.data?.response
|
||||
?: return@launch
|
||||
|
||||
val messagesList = messagesResponse.items
|
||||
if (messagesList.isEmpty()) return@launch
|
||||
|
||||
val normalMessage = messagesList[0].asVkMessage()
|
||||
messagesRepository.store(listOf(normalMessage))
|
||||
|
||||
val profiles = hashMapOf<Int, VkUser>()
|
||||
messagesResponse.profiles?.forEach { baseUser ->
|
||||
baseUser.mapToDomain().let { user -> profiles[user.id] = user }
|
||||
}
|
||||
|
||||
val groups = hashMapOf<Int, VkGroup>()
|
||||
messagesResponse.groups?.forEach { baseGroup ->
|
||||
baseGroup.mapToDomain().let { group -> groups[group.id] = group }
|
||||
}
|
||||
|
||||
val resumeValue: LongPollEvent? = when (eventType) {
|
||||
ApiEvent.MessageNew ->
|
||||
LongPollEvent.VkMessageNewEvent(
|
||||
normalMessage,
|
||||
profiles,
|
||||
groups
|
||||
)
|
||||
ApiEvent.MessageEdit -> LongPollEvent.VkMessageEditEvent(normalMessage)
|
||||
else -> null
|
||||
}
|
||||
|
||||
resumeValue?.let { value -> it.resume(value as T) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
|
||||
listenersMap.let { map ->
|
||||
map[eventType] = (map[eventType] ?: mutableListOf()).also {
|
||||
it.add(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
|
||||
registerListener(ApiEvent.PinUnpinConversation, listener)
|
||||
}
|
||||
|
||||
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
|
||||
onConversationPinStateChanged(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
||||
registerListener(ApiEvent.MessageReadIncoming, listener)
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
|
||||
onMessageIncomingRead(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
|
||||
registerListener(ApiEvent.MessageReadOutgoing, listener)
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
|
||||
onMessageOutgoingRead(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
|
||||
registerListener(ApiEvent.MessageNew, listener)
|
||||
}
|
||||
|
||||
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
|
||||
onNewMessage(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
|
||||
registerListener(ApiEvent.MessageEdit, listener)
|
||||
}
|
||||
|
||||
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
|
||||
onMessageEdited(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun clearListeners() {
|
||||
listenersMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <R : Any> assembleEventCallback(
|
||||
crossinline block: (R) -> Unit,
|
||||
): VkEventCallback<R> {
|
||||
return VkEventCallback { event -> block.invoke(event) }
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
sealed class ActionState {
|
||||
object Phantom : ActionState()
|
||||
object CallInProgress : ActionState()
|
||||
object None : ActionState()
|
||||
|
||||
companion object {
|
||||
fun parse(isPhantom: Boolean, isCallInProgress: Boolean): ActionState {
|
||||
return when {
|
||||
isPhantom -> Phantom
|
||||
isCallInProgress -> CallInProgress
|
||||
else -> None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
sealed class ConversationPeerType : Parcelable {
|
||||
object User : ConversationPeerType()
|
||||
object Group : ConversationPeerType()
|
||||
object Chat : ConversationPeerType()
|
||||
|
||||
fun isUser() = this == User
|
||||
fun isGroup() = this == Group
|
||||
fun isChat() = this == Chat
|
||||
|
||||
companion object {
|
||||
fun parse(type: String): ConversationPeerType {
|
||||
return when (type) {
|
||||
"user" -> User
|
||||
"group" -> Group
|
||||
else -> Chat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkChat(
|
||||
val type: String,
|
||||
val title: String,
|
||||
val adminId: Int,
|
||||
val membersCount: Int,
|
||||
val id: Int,
|
||||
val members: List<ChatMember> = emptyList(),
|
||||
val photo50: String,
|
||||
val photo100: String,
|
||||
val photo200: String,
|
||||
val isDefaultPhoto: Boolean
|
||||
) : Parcelable {
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class ChatMember(
|
||||
val id: Int,
|
||||
val type: ChatMemberType,
|
||||
val isOnline: Boolean?,
|
||||
val lastSeen: Int?,
|
||||
val name: String?,
|
||||
val firstName: String?,
|
||||
val lastName: String?,
|
||||
val invitedBy: Int,
|
||||
val photo50: String?,
|
||||
val photo100: String?,
|
||||
val photo200: String?,
|
||||
val isOwner: Boolean,
|
||||
val isAdmin: Boolean,
|
||||
val canKick: Boolean
|
||||
) : Parcelable {
|
||||
|
||||
fun isProfile(): Boolean = type == ChatMemberType.Profile
|
||||
|
||||
fun isGroup(): Boolean = type == ChatMemberType.Group
|
||||
|
||||
enum class ChatMemberType(val value: String) {
|
||||
Profile("profile"), Group("group");
|
||||
|
||||
companion object {
|
||||
fun parse(value: String) = values().first { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkChatMember(
|
||||
val memberId: Int,
|
||||
val invitedBy: Int,
|
||||
val joinDate: Int,
|
||||
val isAdmin: Boolean,
|
||||
val isOwner: Boolean,
|
||||
val canKick: Boolean
|
||||
) : Parcelable
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "groups")
|
||||
@Parcelize
|
||||
data class VkGroup(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val screenName: String,
|
||||
val photo200: String?,
|
||||
val membersCount: Int?
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString() = name.trim()
|
||||
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||
import com.meloda.fast.model.SelectableItem
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
// TODO: 05.08.2023, Danil Nikolaev: create other class for storing in database
|
||||
@Entity(tableName = "messages")
|
||||
@Parcelize
|
||||
data class VkMessage constructor(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
var id: Int,
|
||||
var text: String? = null,
|
||||
val isOut: Boolean,
|
||||
val peerId: Int,
|
||||
val fromId: Int,
|
||||
val date: Int,
|
||||
val randomId: Int,
|
||||
val action: String? = null,
|
||||
val actionMemberId: Int? = null,
|
||||
val actionText: String? = null,
|
||||
val actionConversationMessageId: Int? = null,
|
||||
val actionMessage: String? = null,
|
||||
|
||||
var updateTime: Int? = null,
|
||||
|
||||
var important: Boolean = false,
|
||||
|
||||
var forwards: List<VkMessage>? = null,
|
||||
var attachments: List<VkAttachment>? = null,
|
||||
var replyMessage: VkMessage? = null,
|
||||
|
||||
val geo: BaseVkMessage.Geo? = null,
|
||||
) : SelectableItem() {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var user: VkUser? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var group: VkGroup? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var actionUser: VkUser? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var actionGroup: VkGroup? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var state: State = State.Sent
|
||||
|
||||
fun isPeerChat() = peerId > 2_000_000_000
|
||||
|
||||
fun isUser() = fromId > 0
|
||||
|
||||
fun isGroup() = fromId < 0
|
||||
|
||||
fun isRead(conversation: VkConversationDomain) =
|
||||
if (isOut) {
|
||||
conversation.outRead - id >= 0
|
||||
} else {
|
||||
conversation.inRead - id >= 0
|
||||
}
|
||||
|
||||
fun getPreparedAction(): Action? {
|
||||
if (action == null) return null
|
||||
return Action.parse(action)
|
||||
}
|
||||
|
||||
fun canEdit() =
|
||||
fromId == UserConfig.userId &&
|
||||
(attachments == null ||
|
||||
!VKConstants.restrictedToEditAttachments.contains(
|
||||
requireNotNull(attachments).first().javaClass
|
||||
)) &&
|
||||
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.OneDayInSeconds)
|
||||
|
||||
fun hasAttachments(): Boolean = !attachments.isNullOrEmpty()
|
||||
|
||||
fun hasReply(): Boolean = replyMessage != null
|
||||
|
||||
fun hasForwards(): Boolean = !forwards.isNullOrEmpty()
|
||||
|
||||
fun hasGeo(): Boolean = geo != null
|
||||
|
||||
fun isUpdated(): Boolean = updateTime != null && requireNotNull(updateTime) > 0
|
||||
|
||||
fun isSending(): Boolean = state == State.Sending
|
||||
|
||||
fun isError(): Boolean = state == State.Error
|
||||
|
||||
fun isSent(): Boolean = state == State.Sent
|
||||
|
||||
enum class Action(val value: String) {
|
||||
CHAT_CREATE("chat_create"),
|
||||
CHAT_PHOTO_UPDATE("chat_photo_update"),
|
||||
CHAT_PHOTO_REMOVE("chat_photo_remove"),
|
||||
CHAT_TITLE_UPDATE("chat_title_update"),
|
||||
CHAT_PIN_MESSAGE("chat_pin_message"),
|
||||
CHAT_UNPIN_MESSAGE("chat_unpin_message"),
|
||||
CHAT_INVITE_USER("chat_invite_user"),
|
||||
CHAT_INVITE_USER_BY_LINK("chat_invite_user_by_link"),
|
||||
CHAT_KICK_USER("chat_kick_user"),
|
||||
CHAT_SCREENSHOT("chat_screenshot"),
|
||||
|
||||
CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"),
|
||||
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"),
|
||||
CHAT_STYLE_UPDATE("conversation_style_update");
|
||||
|
||||
companion object {
|
||||
fun parse(value: String?): Action? = values().firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
enum class State {
|
||||
Sending, Sent, Error
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "users")
|
||||
@Parcelize
|
||||
data class VkUser(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val online: Boolean,
|
||||
val photo200: String?,
|
||||
val lastSeen: Int?,
|
||||
val lastSeenStatus: String?,
|
||||
val birthday: String?
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString() = fullName
|
||||
|
||||
val fullName get() = "$firstName $lastName".trim()
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
open class VkAttachment : Parcelable {
|
||||
|
||||
open fun asString(withAccessKey: Boolean = true) = ""
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkAudio(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val title: String,
|
||||
val artist: String,
|
||||
val url: String,
|
||||
val duration: Int,
|
||||
val accessKey: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
withAccessKey = withAccessKey,
|
||||
accessKey = accessKey
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkCall(
|
||||
val initiatorId: Int,
|
||||
val receiverId: Int,
|
||||
val state: String,
|
||||
val time: Int,
|
||||
val duration: Int,
|
||||
val isVideo: Boolean
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkCurator(
|
||||
val id: Int,
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkEvent(
|
||||
val id: Int
|
||||
) : VkAttachment()
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkFile
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkFile(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val title: String,
|
||||
val ext: String,
|
||||
val size: Int,
|
||||
val url: String,
|
||||
val accessKey: String?,
|
||||
val preview: BaseVkFile.Preview?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
withAccessKey = withAccessKey,
|
||||
accessKey = accessKey
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkGift(
|
||||
val id: Int,
|
||||
val thumb256: String?,
|
||||
val thumb96: String?,
|
||||
val thumb48: String
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkGraffiti(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val url: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val accessKey: String
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkGroupCall(
|
||||
val initiatorId: Int
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkLink(
|
||||
val url: String,
|
||||
val title: String?,
|
||||
val caption: String?,
|
||||
val photo: VkPhoto?,
|
||||
val target: String?,
|
||||
val isFavorite: Boolean
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkMiniApp(
|
||||
val link: String
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import androidx.room.Ignore
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
data class VkPhoto(
|
||||
val albumId: Int,
|
||||
val date: Int,
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val hasTags: Boolean,
|
||||
val accessKey: String?,
|
||||
val sizes: List<BaseVkPhoto.Size>,
|
||||
val text: String?,
|
||||
val userId: Int?
|
||||
) : VkAttachment() {
|
||||
|
||||
companion object {
|
||||
const val SIZE_TYPE_75 = 's'
|
||||
const val SIZE_TYPE_130 = 'm'
|
||||
const val SIZE_TYPE_604 = 'x'
|
||||
const val SIZE_TYPE_807 = 'y'
|
||||
const val SIZE_TYPE_1080_1024 = 'z'
|
||||
const val SIZE_TYPE_2560_2048 = 'w'
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
private val sizesChars = Stack<Char>()
|
||||
|
||||
init {
|
||||
sizesChars.push(SIZE_TYPE_75)
|
||||
sizesChars.push(SIZE_TYPE_130)
|
||||
sizesChars.push(SIZE_TYPE_604)
|
||||
sizesChars.push('o')
|
||||
sizesChars.push('p')
|
||||
sizesChars.push('q')
|
||||
sizesChars.push('r')
|
||||
sizesChars.push(SIZE_TYPE_807)
|
||||
sizesChars.push(SIZE_TYPE_1080_1024)
|
||||
sizesChars.push(SIZE_TYPE_2560_2048)
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
withAccessKey = withAccessKey,
|
||||
accessKey = accessKey
|
||||
)
|
||||
|
||||
fun getMaxSize(): BaseVkPhoto.Size? {
|
||||
return getSizeOrSmaller(sizesChars.peek())
|
||||
}
|
||||
|
||||
fun getSizeOrNull(type: Char): BaseVkPhoto.Size? {
|
||||
for (size in sizes) {
|
||||
if (size.type == type.toString()) return size
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? {
|
||||
val photoStack = sizesChars.clone() as Stack<*>
|
||||
|
||||
val sizeIndex = photoStack.search(type)
|
||||
|
||||
if (sizeIndex == -1) return null
|
||||
|
||||
for (i in 0 until sizeIndex) {
|
||||
photoStack.pop()
|
||||
}
|
||||
|
||||
for (i in 0 until photoStack.size) {
|
||||
val size = getSizeOrNull(photoStack.peek() as Char)
|
||||
|
||||
if (size == null) {
|
||||
photoStack.pop()
|
||||
continue
|
||||
} else {
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkPoll(
|
||||
val id: Int
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkSticker(
|
||||
val id: Int,
|
||||
val productId: Int,
|
||||
val images: List<BaseVkSticker.Image>,
|
||||
val backgroundImages: List<BaseVkSticker.Image>
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
fun urlForSize(size: Int): String? {
|
||||
for (image in images) {
|
||||
if (image.width == size) return image.url
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkStory(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val date: Int,
|
||||
val photo: VkPhoto?
|
||||
) : VkAttachment() {
|
||||
|
||||
fun isFromUser() = ownerId > 0
|
||||
|
||||
fun isFromGroup() = ownerId < 0
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkVideo(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val images: List<VideoImage>,
|
||||
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
||||
val accessKey: String?,
|
||||
val title: String,
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
fun imageForWidth(width: Int): VideoImage? {
|
||||
return images.find { it.width == width }
|
||||
}
|
||||
|
||||
fun imageForWidthAtLeast(width: Int): VideoImage? {
|
||||
var certainImages = images.sortedByDescending { it.width }
|
||||
var containsVertical = false
|
||||
for (image in images) {
|
||||
if (image.shapeKind == ShapeKind.Vertical) {
|
||||
containsVertical = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (containsVertical) {
|
||||
certainImages = certainImages.filter { it.shapeKind == ShapeKind.Vertical }
|
||||
}
|
||||
|
||||
certainImages = certainImages.filter { it.width >= width }
|
||||
|
||||
return certainImages.firstOrNull()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class VideoImage(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val withPadding: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
var shapeKind: ShapeKind? = null
|
||||
|
||||
init {
|
||||
val ratio = width.toFloat() / height.toFloat()
|
||||
|
||||
shapeKind = when {
|
||||
ratio > 1 -> ShapeKind.Horizontal
|
||||
ratio < 1 -> ShapeKind.Vertical
|
||||
else -> ShapeKind.Square
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class ShapeKind(val value: Int) {
|
||||
object Square : ShapeKind(0)
|
||||
object Vertical : ShapeKind(1)
|
||||
object Horizontal : ShapeKind(2)
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
fun parse(value: Int) = when (value) {
|
||||
0 -> Square
|
||||
1 -> Vertical
|
||||
2 -> Horizontal
|
||||
else -> throw IllegalArgumentException("Unknown value: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
withAccessKey = withAccessKey,
|
||||
accessKey = accessKey
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkVoiceMessage(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val duration: Int,
|
||||
val waveform: List<Int>,
|
||||
val linkOgg: String,
|
||||
val linkMp3: String,
|
||||
val accessKey: String,
|
||||
val transcriptState: String?,
|
||||
val transcript: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkWall(
|
||||
val id: Int,
|
||||
val fromId: Int,
|
||||
val toId: Int,
|
||||
val date: Int,
|
||||
val text: String,
|
||||
val attachments: List<BaseVkAttachmentItem>?,
|
||||
val comments: Int?,
|
||||
val likes: Int?,
|
||||
val reposts: Int?,
|
||||
val views: Int?,
|
||||
val isFavorite: Boolean,
|
||||
val accessKey: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkWallReply(
|
||||
val id: Int
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkWidget(
|
||||
val id: Int
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.meloda.fast.api.model.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.VkChat
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkChat(
|
||||
val type: String,
|
||||
val title: String,
|
||||
val admin_id: Int,
|
||||
val members_count: Int,
|
||||
val id: Int,
|
||||
val photo_50: String,
|
||||
val photo_100: String,
|
||||
val photo_200: String,
|
||||
val is_default_photo: Boolean,
|
||||
val push_settings: PushSettings
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkChat() = VkChat(
|
||||
type = type,
|
||||
title = title,
|
||||
adminId = admin_id,
|
||||
membersCount = members_count,
|
||||
id = id,
|
||||
photo50 = photo_50,
|
||||
photo100 = photo_100,
|
||||
photo200 = photo_200,
|
||||
isDefaultPhoto = is_default_photo
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class PushSettings(
|
||||
val sound: Int,
|
||||
val disabled_until: Int
|
||||
) : Parcelable
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.model.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.VkChatMember
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkChatMember(
|
||||
val member_id: Int,
|
||||
val invited_by: Int,
|
||||
val join_date: Int,
|
||||
val is_admin: Boolean?,
|
||||
val is_owner: Boolean?,
|
||||
val can_kick: Boolean?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkChatMember() = VkChatMember(
|
||||
memberId = member_id,
|
||||
invitedBy = invited_by,
|
||||
joinDate = join_date,
|
||||
isAdmin = is_admin == true,
|
||||
isOwner = is_owner == true,
|
||||
canKick = can_kick == true
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.meloda.fast.api.model.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkGroup(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val screen_name: String,
|
||||
val is_closed: Int,
|
||||
val type: String,
|
||||
val is_admin: Int,
|
||||
val is_member: Int,
|
||||
val is_advertiser: Int,
|
||||
val photo_50: String?,
|
||||
val photo_100: String?,
|
||||
val photo_200: String?,
|
||||
val members_count: Int?
|
||||
) : Parcelable {
|
||||
|
||||
fun mapToDomain() = VkGroup(
|
||||
id = -id,
|
||||
name = name,
|
||||
screenName = screen_name,
|
||||
photo200 = photo_200,
|
||||
membersCount = members_count
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.meloda.fast.api.model.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkLongPoll(
|
||||
val server: String,
|
||||
val key: String,
|
||||
val ts: Int,
|
||||
val pts: Int
|
||||
) : Parcelable
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.meloda.fast.api.model.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkMessage(
|
||||
val id: Int,
|
||||
val peer_id: Int,
|
||||
val date: Int,
|
||||
val from_id: Int,
|
||||
val out: Int,
|
||||
val text: String,
|
||||
val conversation_message_id: Int,
|
||||
val fwd_messages: List<BaseVkMessage>? = emptyList(),
|
||||
val important: Boolean,
|
||||
val random_id: Int,
|
||||
val attachments: List<BaseVkAttachmentItem> = emptyList(),
|
||||
val is_hidden: Boolean,
|
||||
val payload: String,
|
||||
val geo: Geo?,
|
||||
val action: Action?,
|
||||
val ttl: Int,
|
||||
val reply_message: BaseVkMessage?,
|
||||
val update_time: Int?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkMessage() = VkMessage(
|
||||
id = id,
|
||||
text = text.ifBlank { null },
|
||||
isOut = out == 1,
|
||||
peerId = peer_id,
|
||||
fromId = from_id,
|
||||
date = date,
|
||||
randomId = random_id,
|
||||
action = action?.type,
|
||||
actionMemberId = action?.member_id,
|
||||
actionText = action?.text,
|
||||
actionConversationMessageId = action?.conversation_message_id,
|
||||
actionMessage = action?.message,
|
||||
geo = geo,
|
||||
important = important,
|
||||
updateTime = update_time
|
||||
).also {
|
||||
it.attachments = VkUtils.parseAttachments(attachments)
|
||||
it.forwards = VkUtils.parseForwards(fwd_messages)
|
||||
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Geo(
|
||||
val type: String,
|
||||
val coordinates: Coordinates,
|
||||
val place: Place
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Place(val country: String, val city: String, val title: String) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Action(
|
||||
val type: String,
|
||||
val member_id: Int?,
|
||||
val text: String?,
|
||||
val conversation_message_id: Int?,
|
||||
val message: String?
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.meloda.fast.api.model.base
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkUser(
|
||||
val id: Int,
|
||||
val first_name: String,
|
||||
val last_name: String,
|
||||
val can_access_closed: Boolean,
|
||||
val is_closed: Boolean,
|
||||
val can_invite_to_chats: Boolean,
|
||||
val sex: Int?,
|
||||
val photo_50: String?,
|
||||
val photo_100: String?,
|
||||
val photo_200: String?,
|
||||
val online: Int?,
|
||||
val online_info: OnlineInfo?,
|
||||
val screen_name: String,
|
||||
val bdate: String?
|
||||
//...other fields
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class OnlineInfo(
|
||||
val visible: Boolean,
|
||||
val status: String,
|
||||
val last_seen: Int?,
|
||||
val is_online: Boolean?,
|
||||
val online_mobile: Boolean?,
|
||||
val app_id: Int?
|
||||
) : Parcelable
|
||||
|
||||
fun mapToDomain() = VkUser(
|
||||
id = id,
|
||||
firstName = first_name,
|
||||
lastName = last_name,
|
||||
online = online == 1,
|
||||
photo200 = photo_200,
|
||||
lastSeen = online_info?.last_seen,
|
||||
lastSeenStatus = online_info?.status,
|
||||
birthday = bdate
|
||||
)
|
||||
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkAttachmentItem(
|
||||
val type: String,
|
||||
val photo: BaseVkPhoto?,
|
||||
val video: BaseVkVideo?,
|
||||
val audio: BaseVkAudio?,
|
||||
@SerializedName("doc")
|
||||
val file: BaseVkFile?,
|
||||
val link: BaseVkLink?,
|
||||
@SerializedName("mini_app")
|
||||
val miniApp: BaseVkMiniApp?,
|
||||
@SerializedName("audio_message")
|
||||
val voiceMessage: BaseVkVoiceMessage?,
|
||||
val sticker: BaseVkSticker?,
|
||||
val gift: BaseVkGift?,
|
||||
val wall: BaseVkWall?,
|
||||
val graffiti: BaseVkGraffiti?,
|
||||
val poll: BaseVkPoll?,
|
||||
@SerializedName("wall_reply")
|
||||
val wallReply: BaseVkWallReply?,
|
||||
val call: BaseVkCall?,
|
||||
@SerializedName("group_call_in_progress")
|
||||
val groupCall: BaseVkGroupCall?,
|
||||
val curator: BaseVkCurator?,
|
||||
val event: BaseVkEvent?,
|
||||
val story: BaseVkStory?,
|
||||
val widget: BaseVkWidget?
|
||||
) : Parcelable {
|
||||
|
||||
fun getPreparedType() = AttachmentType.parse(type)
|
||||
|
||||
enum class AttachmentType(var value: String) {
|
||||
Unknown("unknown"),
|
||||
Photo("photo"),
|
||||
Video("video"),
|
||||
Audio("audio"),
|
||||
File("doc"),
|
||||
Link("link"),
|
||||
Voice("audio_message"),
|
||||
MiniApp("mini_app"),
|
||||
Sticker("sticker"),
|
||||
Gift("gift"),
|
||||
Wall("wall"),
|
||||
Graffiti("graffiti"),
|
||||
Poll("poll"),
|
||||
WallReply("wall_reply"),
|
||||
Call("call"),
|
||||
GroupCallInProgress("group_call_in_progress"),
|
||||
Curator("curator"),
|
||||
Event("event"),
|
||||
Story("story"),
|
||||
Widget("widget")
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: String): AttachmentType {
|
||||
val parsedValue = values().firstOrNull { it.value == value } ?: Unknown
|
||||
|
||||
if (parsedValue == Unknown) {
|
||||
Log.e("AttachmentType", "Unknown attachment type: $value")
|
||||
}
|
||||
|
||||
return parsedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class BaseVkAttachment : Parcelable
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkAudio
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkAudio(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val artist: String,
|
||||
val duration: Int,
|
||||
val url: String,
|
||||
val date: Int,
|
||||
val owner_id: Int,
|
||||
val access_key: String?,
|
||||
val is_explicit: Boolean,
|
||||
val is_focus_track: Boolean,
|
||||
val is_licensed: Boolean,
|
||||
val track_code: String,
|
||||
val genre_id: Int,
|
||||
val album: Album,
|
||||
val short_videos_allowed: Boolean,
|
||||
val stories_allowed: Boolean,
|
||||
val stories_cover_allowed: Boolean
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkAudio() = VkAudio(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
title = title,
|
||||
artist = artist,
|
||||
url = url,
|
||||
duration = duration,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Album(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val owner_id: Int,
|
||||
val access_key: String,
|
||||
val thumb: Thumb
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Thumb(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val photo_34: String,
|
||||
val photo_68: String,
|
||||
val photo_135: String,
|
||||
val photo_270: String,
|
||||
val photo_300: String,
|
||||
val photo_600: String,
|
||||
val photo_1200: String
|
||||
) : Parcelable
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkCall
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkCall(
|
||||
val initiator_id: Int,
|
||||
val receiver_id: Int,
|
||||
val state: String,
|
||||
val time: Int,
|
||||
val duration: Int,
|
||||
val video: Boolean
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkCall() = VkCall(
|
||||
initiatorId = initiator_id,
|
||||
receiverId = receiver_id,
|
||||
state = state,
|
||||
time = time,
|
||||
duration = duration,
|
||||
isVideo = video
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkCurator
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkCurator(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val url: String,
|
||||
val photo: List<Photo>
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkCurator() = VkCurator(
|
||||
id = id
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Photo(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: String
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import com.meloda.fast.api.model.attachments.VkEvent
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkEvent(
|
||||
val button_text: String,
|
||||
val id: Int,
|
||||
val is_favorite: Boolean,
|
||||
val text: String,
|
||||
val address: String,
|
||||
val friends: List<Int> = emptyList(),
|
||||
val member_status: Int,
|
||||
val time: Int
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkEvent() = VkEvent(id = id)
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkFile
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkFile(
|
||||
val id: Int,
|
||||
val owner_id: Int,
|
||||
val title: String,
|
||||
val size: Int,
|
||||
val ext: String,
|
||||
val date: Int,
|
||||
val type: Int,
|
||||
val url: String,
|
||||
val preview: Preview?,
|
||||
val ic_licensed: Int,
|
||||
val access_key: String?,
|
||||
val web_preview_url: String?
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkFile() = VkFile(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
title = title,
|
||||
ext = ext,
|
||||
url = url,
|
||||
size = size,
|
||||
accessKey = access_key,
|
||||
preview = preview
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Preview(
|
||||
val photo: Photo?,
|
||||
val video: Video?
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Photo(val sizes: List<Size>) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Size(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val type: String,
|
||||
val src: String
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Video(
|
||||
val src: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val file_size: Int
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkGift
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkGift(
|
||||
val id: Int,
|
||||
val thumb_256: String?,
|
||||
val thumb_96: String?,
|
||||
val thumb_48: String
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkGift() = VkGift(
|
||||
id = id,
|
||||
thumb256 = thumb_256,
|
||||
thumb96 = thumb_96,
|
||||
thumb48 = thumb_48
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkGraffiti
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkGraffiti(
|
||||
val id: Int,
|
||||
val owner_id: Int,
|
||||
val url: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val access_key: String
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkGraffiti() = VkGraffiti(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
url = url,
|
||||
width = width,
|
||||
height = height,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkGroupCall
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkGroupCall(
|
||||
val initiator_id: Int,
|
||||
val join_link: String,
|
||||
val participants: Participants
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Participants(
|
||||
val list: List<Int>,
|
||||
val count: Int
|
||||
) : Parcelable
|
||||
|
||||
fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id)
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import com.meloda.fast.api.model.attachments.VkLink
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkLink(
|
||||
val url: String,
|
||||
val title: String?,
|
||||
val caption: String?,
|
||||
val photo: BaseVkPhoto?,
|
||||
val target: String?,
|
||||
val is_favorite: Boolean
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkLink() = VkLink(
|
||||
url = url,
|
||||
title = title,
|
||||
caption = caption,
|
||||
photo = photo?.asVkPhoto(),
|
||||
target = target,
|
||||
isFavorite = is_favorite
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.api.model.attachments.VkMiniApp
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkMiniApp(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val app: App,
|
||||
val images: List<Image>?,
|
||||
val button_text: String
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class App(
|
||||
val type: String,
|
||||
val id: Int,
|
||||
val title: String,
|
||||
@SerializedName("author_owner_id")
|
||||
val authorOwnerId: Int,
|
||||
@SerializedName("are_notifications_enabled")
|
||||
val areNotificationsEnabled: Boolean,
|
||||
@SerializedName("is_favorite")
|
||||
val isFavorite: Boolean,
|
||||
@SerializedName("is_installed")
|
||||
val isInstalled: Boolean,
|
||||
@SerializedName("track_code")
|
||||
val trackCode: String,
|
||||
@SerializedName("share_url")
|
||||
val shareUrl: String,
|
||||
@SerializedName("webview_url")
|
||||
val webViewUrl: String,
|
||||
@SerializedName("hide_tabbar")
|
||||
val hideTabBar: Int,
|
||||
@SerializedName("icon_75")
|
||||
val icon75: String?,
|
||||
@SerializedName("icon_139")
|
||||
val icon139: String?,
|
||||
@SerializedName("icon_150")
|
||||
val icon150: String?,
|
||||
@SerializedName("icon_278")
|
||||
val icon278: String?,
|
||||
@SerializedName("icon_576")
|
||||
val icon576: String?,
|
||||
@SerializedName("open_in_external_browser")
|
||||
val openInExternalBrowser: Boolean,
|
||||
@SerializedName("need_policy_confirmation")
|
||||
val needPolicyConfirmation: Boolean,
|
||||
@SerializedName("is_vkui_internal")
|
||||
val isVkUiInternal: Boolean,
|
||||
@SerializedName("has_vk_connect")
|
||||
val hasVkConnect: Boolean,
|
||||
@SerializedName("need_show_bottom_menu_tooltip_on_close")
|
||||
val needShowBottomMenuTooltipOnClose: Boolean
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Image(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
fun asVkMiniApp() = VkMiniApp(link = app.shareUrl)
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkPhoto
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkPhoto(
|
||||
val album_id: Int,
|
||||
val date: Int,
|
||||
val id: Int,
|
||||
val owner_id: Int,
|
||||
val has_tags: Boolean,
|
||||
val access_key: String?,
|
||||
val sizes: List<Size>,
|
||||
val text: String?,
|
||||
val user_id: Int?,
|
||||
val lat: Double?,
|
||||
val long: Double?,
|
||||
val post_id: Int?
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkPhoto() = VkPhoto(
|
||||
albumId = album_id,
|
||||
date = date,
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
hasTags = has_tags,
|
||||
accessKey = access_key,
|
||||
sizes = sizes,
|
||||
text = text,
|
||||
userId = user_id
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Size(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val type: String,
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkPoll
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkPoll(
|
||||
val multiple: Boolean,
|
||||
val id: Int,
|
||||
val votes: Int,
|
||||
val anonymous: Boolean,
|
||||
val closed: Boolean,
|
||||
val end_date: Int,
|
||||
val is_board: Boolean,
|
||||
val can_vote: Boolean,
|
||||
val can_edit: Boolean,
|
||||
val can_report: Boolean,
|
||||
val can_share: Boolean,
|
||||
val created: Int,
|
||||
val owner_id: Int,
|
||||
val question: String,
|
||||
val disable_unvote: Boolean,
|
||||
val friends: List<Friend>?,
|
||||
val embed_hash: String,
|
||||
val answers: List<Answer>,
|
||||
val author_id: Int,
|
||||
val background: Background?
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Friend(
|
||||
val id: Int
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Answer(
|
||||
val id: Int,
|
||||
val rate: Double,
|
||||
val text: String,
|
||||
val votes: Int
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Background(
|
||||
val angle: Int,
|
||||
val color: String,
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val type: String,
|
||||
val points: List<Point>
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Point(
|
||||
val color: String,
|
||||
val position: Double
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
fun asVkPoll() = VkPoll(id = id)
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkSticker
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkSticker(
|
||||
val product_id: Int,
|
||||
val sticker_id: Int,
|
||||
val images: List<Image>,
|
||||
val images_with_background: List<Image>,
|
||||
val animation_url: String?,
|
||||
val animations: List<Animation>?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkSticker() = VkSticker(
|
||||
id = sticker_id,
|
||||
productId = product_id,
|
||||
images = images,
|
||||
backgroundImages = images_with_background
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Image(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Animation(
|
||||
val type: String,
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkStory
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkStory(
|
||||
val id: Int,
|
||||
val owner_id: Int,
|
||||
val access_key: String,
|
||||
val can_comment: Int,
|
||||
val can_reply: Int,
|
||||
val can_like: Boolean,
|
||||
val can_share: Int,
|
||||
val can_hide: Int,
|
||||
val date: Int,
|
||||
val expires_at: Int,
|
||||
val is_ads: Boolean,
|
||||
val photo: BaseVkPhoto?,
|
||||
val replies: Replies,
|
||||
val is_one_time: Boolean,
|
||||
val track_code: String,
|
||||
val type: String,
|
||||
val views: Int,
|
||||
val likes_count: Int,
|
||||
val reaction_set_id: String,
|
||||
val is_restricted: Boolean,
|
||||
val no_sound: Boolean,
|
||||
val need_mute: Boolean,
|
||||
val mute_reply: Boolean,
|
||||
val can_ask: Int,
|
||||
val can_ask_anonymous: Int,
|
||||
val preloading_enabled: Boolean,
|
||||
val narratives_count: Int,
|
||||
val can_use_in_narrative: Boolean
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkStory() = VkStory(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
date = date,
|
||||
photo = photo?.asVkPhoto()
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Replies(
|
||||
val count: Int,
|
||||
val new: Int
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkVideo
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkVideo(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val duration: Int,
|
||||
val date: Int,
|
||||
val comments: Int,
|
||||
val description: String,
|
||||
val player: String,
|
||||
val added: Int,
|
||||
val type: String,
|
||||
val views: Int,
|
||||
val can_comment: Int,
|
||||
val can_edit: Int,
|
||||
val can_like: Int,
|
||||
val can_repost: Int,
|
||||
val can_subscribe: Int,
|
||||
val can_add_to_faves: Int,
|
||||
val can_add: Int,
|
||||
val can_attach_link: Int,
|
||||
val access_key: String?,
|
||||
val owner_id: Int,
|
||||
val ov_id: String,
|
||||
val is_favorite: Boolean,
|
||||
val track_code: String,
|
||||
val image: List<Image>,
|
||||
val first_frame: List<FirstFrame>,
|
||||
val files: File,
|
||||
val timeline_thumbs: TimelineThumbs,
|
||||
val ads: Ads
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkVideo() = VkVideo(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
images = image.map { it.asVideoImage() },
|
||||
firstFrames = first_frame,
|
||||
accessKey = access_key,
|
||||
title = title
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Image(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val with_padding: Int?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVideoImage() = VkVideo.VideoImage(
|
||||
width = width,
|
||||
height = height,
|
||||
url = url,
|
||||
withPadding = with_padding == 1
|
||||
)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class FirstFrame(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class File(
|
||||
val mp4_240: String?,
|
||||
val mp4_360: String?,
|
||||
val mp4_480: String?,
|
||||
val mp4_720: String?,
|
||||
val mp4_1080: String?,
|
||||
val mp4_1440: String?,
|
||||
val hls: String,
|
||||
val dash_uni: String,
|
||||
val dash_sep: String,
|
||||
val hls_ondemand: String,
|
||||
val dash_ondemand: String,
|
||||
val failover_host: String
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class TimelineThumbs(
|
||||
val count_per_image: Int,
|
||||
val count_per_row: Int,
|
||||
val count_total: Int,
|
||||
val frame_height: Int,
|
||||
val frame_width: Float,
|
||||
val links: List<String>,
|
||||
val is_uv: Boolean,
|
||||
val frequency: Int
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Ads(
|
||||
val slot_id: Int,
|
||||
val timeout: Int,
|
||||
val can_play: Int,
|
||||
val params: Params,
|
||||
val sections: List<String>,
|
||||
val midroll_percents: List<Float>
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Params(
|
||||
val vk_id: Int,
|
||||
val duration: Int,
|
||||
val video_id: String,
|
||||
val pl: Int,
|
||||
val content_id: String,
|
||||
val lang: Int,
|
||||
val puid1: String,
|
||||
val puid2: Int,
|
||||
val puid3: Int,
|
||||
val puid5: Int,
|
||||
val puid6: Int,
|
||||
val puid7: Int,
|
||||
val puid9: Int,
|
||||
val puid10: Int,
|
||||
val puid12: Int,
|
||||
val puid13: Int,
|
||||
val puid14: Int,
|
||||
val puid15: Int,
|
||||
val puid18: Int,
|
||||
val puid21: Int,
|
||||
val sign: String,
|
||||
val groupId: Int,
|
||||
val vk_catid: Int,
|
||||
val is_xz_video: Int
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkVoiceMessage
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkVoiceMessage(
|
||||
val id: Int,
|
||||
val owner_id: Int,
|
||||
val duration: Int,
|
||||
val waveform: List<Int>,
|
||||
val link_ogg: String,
|
||||
val link_mp3: String,
|
||||
val access_key: String,
|
||||
val transcript_state: String?,
|
||||
val transcript: String?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkVoiceMessage() = VkVoiceMessage(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
duration = duration,
|
||||
waveform = waveform,
|
||||
linkOgg = link_ogg,
|
||||
linkMp3 = link_mp3,
|
||||
accessKey = access_key,
|
||||
transcriptState = transcript_state,
|
||||
transcript = transcript
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkWall
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkWall(
|
||||
val id: Int,
|
||||
val from_id: Int,
|
||||
val to_id: Int,
|
||||
val date: Int,
|
||||
val text: String,
|
||||
val attachments: List<BaseVkAttachmentItem>?,
|
||||
val post_source: PostSource?,
|
||||
val comments: Comments?,
|
||||
val likes: Likes?,
|
||||
val reposts: Reposts?,
|
||||
val views: Views?,
|
||||
val is_favorite: Boolean,
|
||||
val donut: Donut?,
|
||||
val access_key: String?,
|
||||
val short_text_rate: Double
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkWall() = VkWall(
|
||||
id = id,
|
||||
fromId = from_id,
|
||||
toId = to_id,
|
||||
date = date,
|
||||
text = text,
|
||||
attachments = attachments,
|
||||
comments = comments?.count,
|
||||
likes = likes?.count,
|
||||
reposts = reposts?.count,
|
||||
views = views?.count,
|
||||
isFavorite = is_favorite,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class PostSource(
|
||||
val type: String,
|
||||
val platform: String
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Comments(
|
||||
val count: Int,
|
||||
val can_post: Int,
|
||||
val groups_can_post: Boolean
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Likes(
|
||||
val count: Int,
|
||||
val user_likes: Int,
|
||||
val can_like: Int,
|
||||
val can_publish: Int,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Reposts(
|
||||
val count: Int,
|
||||
val user_reposted: Int
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Views(
|
||||
val count: Int
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Donut(
|
||||
val is_donut: Boolean
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkWallReply
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkWallReply(
|
||||
val id: Int,
|
||||
val from_id: Int,
|
||||
val date: Int,
|
||||
val text: String,
|
||||
val post_id: Int,
|
||||
val owner_id: Int,
|
||||
val parents_stack: List<Int>,
|
||||
val likes: Likes,
|
||||
val reply_to_user: Int?,
|
||||
val reply_to_comment: Int?
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Likes(
|
||||
val count: Int,
|
||||
val can_like: Int,
|
||||
val user_likes: Int,
|
||||
val can_publish: Int
|
||||
) : Parcelable
|
||||
|
||||
fun asVkWallReply() = VkWallReply(id = id)
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import com.meloda.fast.api.model.attachments.VkWidget
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkWidget(val id: Int) : BaseVkAttachment() {
|
||||
|
||||
fun asVkWidget() = VkWidget(id)
|
||||
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package com.meloda.fast.api.model.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
|
||||
import com.meloda.fast.api.model.domain.VkConversationDomain
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkConversation(
|
||||
val peer: Peer,
|
||||
val last_message_id: Int,
|
||||
val in_read: Int,
|
||||
val out_read: Int,
|
||||
val in_read_cmid: Int,
|
||||
val out_read_cmid: Int,
|
||||
val sort_id: SortId,
|
||||
val last_conversation_message_id: Int,
|
||||
val is_marked_unread: Boolean,
|
||||
val important: Boolean,
|
||||
val push_settings: PushSettings,
|
||||
val can_write: CanWrite,
|
||||
val can_send_money: Boolean,
|
||||
val can_receive_money: Boolean,
|
||||
val chat_settings: ChatSettings?,
|
||||
val call_in_progress: CallInProgress?,
|
||||
val unread_count: Int?,
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Peer(
|
||||
val id: Int,
|
||||
val type: String,
|
||||
val local_id: Int,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class SortId(
|
||||
val major_id: Int,
|
||||
val minor_id: Int,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class PushSettings(
|
||||
val disabled_forever: Boolean,
|
||||
val no_sound: Boolean,
|
||||
val disabled_mentions: Boolean,
|
||||
val disabled_mass_mentions: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class CanWrite(
|
||||
val allowed: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class ChatSettings(
|
||||
val owner_id: Int,
|
||||
val title: String,
|
||||
val state: String,
|
||||
val acl: Acl,
|
||||
val members_count: Int,
|
||||
val friends_count: Int,
|
||||
val photo: Photo?,
|
||||
val admin_ids: List<Int>,
|
||||
val active_ids: List<Int>,
|
||||
val is_group_channel: Boolean,
|
||||
val is_disappearing: Boolean,
|
||||
val is_service: Boolean,
|
||||
val theme: String?,
|
||||
val pinned_message: BaseVkMessage?,
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Acl(
|
||||
val can_change_info: Boolean,
|
||||
val can_change_invite_link: Boolean,
|
||||
val can_change_pin: Boolean,
|
||||
val can_invite: Boolean,
|
||||
val can_promote_users: Boolean,
|
||||
val can_see_invite_link: Boolean,
|
||||
val can_moderate: Boolean,
|
||||
val can_copy_chat: Boolean,
|
||||
val can_call: Boolean,
|
||||
val can_use_mass_mentions: Boolean,
|
||||
val can_change_style: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Photo(
|
||||
val photo_50: String?,
|
||||
val photo_100: String?,
|
||||
val photo_200: String?,
|
||||
val is_default_photo: Boolean,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class CallInProgress(
|
||||
val participants: BaseVkGroupCall.Participants,
|
||||
val join_link: String,
|
||||
) : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Participants(
|
||||
val list: List<Int>,
|
||||
val count: Int,
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
|
||||
fun mapToDomain(
|
||||
lastMessage: VkMessage? = null,
|
||||
conversationUser: VkUser? = null,
|
||||
conversationGroup: VkGroup? = null,
|
||||
) = VkConversationDomain(
|
||||
id = peer.id,
|
||||
localId = peer.local_id,
|
||||
conversationTitle = chat_settings?.title,
|
||||
conversationPhoto = chat_settings?.photo?.photo_200,
|
||||
type = peer.type,
|
||||
isCallInProgress = call_in_progress != null,
|
||||
isPhantom = chat_settings?.is_disappearing == true,
|
||||
lastConversationMessageId = last_conversation_message_id,
|
||||
inRead = in_read,
|
||||
outRead = out_read,
|
||||
lastMessageId = last_message_id,
|
||||
unreadCount = unread_count ?: 0,
|
||||
membersCount = chat_settings?.members_count,
|
||||
ownerId = chat_settings?.owner_id,
|
||||
majorId = sort_id.major_id,
|
||||
minorId = sort_id.minor_id,
|
||||
canChangePin = chat_settings?.acl?.can_change_pin == true,
|
||||
canChangeInfo = chat_settings?.acl?.can_change_info == true,
|
||||
pinnedMessageId = chat_settings?.pinned_message?.id,
|
||||
inReadCmId = in_read_cmid,
|
||||
outReadCmId = out_read_cmid,
|
||||
).also {
|
||||
it.lastMessage = lastMessage
|
||||
it.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
||||
it.conversationUser = conversationUser
|
||||
it.conversationGroup = conversationGroup
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
package com.meloda.fast.api.model.domain
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.ActionState
|
||||
import com.meloda.fast.api.model.ConversationPeerType
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.presentation.VkConversationUi
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.ext.isFalse
|
||||
import com.meloda.fast.ext.isTrue
|
||||
import com.meloda.fast.ext.orDots
|
||||
import com.meloda.fast.model.base.UiImage
|
||||
import com.meloda.fast.model.base.UiText
|
||||
import com.meloda.fast.model.base.parseString
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.Calendar
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
@Entity(tableName = "conversations")
|
||||
@Parcelize
|
||||
data class VkConversationDomain(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val localId: Int,
|
||||
val ownerId: Int?,
|
||||
val conversationTitle: String?,
|
||||
val conversationPhoto: String?,
|
||||
val isCallInProgress: Boolean,
|
||||
val isPhantom: Boolean,
|
||||
val lastConversationMessageId: Int,
|
||||
val inReadCmId: Int,
|
||||
val outReadCmId: Int,
|
||||
val inRead: Int,
|
||||
val outRead: Int,
|
||||
val lastMessageId: Int,
|
||||
val unreadCount: Int,
|
||||
val membersCount: Int?,
|
||||
val canChangePin: Boolean,
|
||||
val canChangeInfo: Boolean,
|
||||
val majorId: Int,
|
||||
val minorId: Int,
|
||||
val pinnedMessageId: Int?,
|
||||
val type: String,
|
||||
) : Parcelable {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var peerType: ConversationPeerType = ConversationPeerType.parse(type)
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var lastMessage: VkMessage? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var pinnedMessage: VkMessage? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var conversationUser: VkUser? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var conversationGroup: VkGroup? = null
|
||||
|
||||
fun isChat() = peerType.isChat()
|
||||
fun isUser() = peerType.isUser()
|
||||
fun isGroup() = peerType.isGroup()
|
||||
|
||||
fun isInUnread() = inRead - lastMessageId < 0
|
||||
fun isOutUnread() = outRead - lastMessageId < 0
|
||||
|
||||
fun isUnread() = isInUnread() || isOutUnread()
|
||||
|
||||
fun isAccount() = id == UserConfig.userId
|
||||
|
||||
fun isPinned() = majorId > 0
|
||||
|
||||
fun extractAvatar(): UiImage {
|
||||
val placeholderImage = UiImage.Resource(R.drawable.ic_account_circle_cut)
|
||||
|
||||
val avatarLink = when {
|
||||
peerType.isUser() -> {
|
||||
if (id == UserConfig.userId) {
|
||||
null
|
||||
} else {
|
||||
conversationUser?.photo200
|
||||
}
|
||||
}
|
||||
|
||||
peerType.isGroup() -> conversationGroup?.photo200
|
||||
peerType.isChat() -> conversationPhoto
|
||||
else -> null
|
||||
}
|
||||
|
||||
return avatarLink?.let(UiImage::Url) ?: placeholderImage
|
||||
}
|
||||
|
||||
fun extractTitle(): UiText {
|
||||
return when {
|
||||
isAccount() -> UiText.Resource(R.string.favorites)
|
||||
peerType.isChat() -> UiText.Simple(conversationTitle ?: "...")
|
||||
peerType.isUser() -> UiText.Simple(conversationUser?.fullName ?: "...")
|
||||
peerType.isGroup() -> UiText.Simple(conversationGroup?.name ?: "...")
|
||||
else -> UiText.Simple("...")
|
||||
}
|
||||
}
|
||||
|
||||
fun extractUnreadCounterText(): String? {
|
||||
if (lastMessage?.isOut.isFalse && !isInUnread()) return null
|
||||
|
||||
return when (unreadCount) {
|
||||
in 1..999 -> unreadCount.toString()
|
||||
0 -> null
|
||||
else -> "%dK".format(unreadCount / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 07.01.2023, Danil Nikolaev: rewrite
|
||||
fun extractMessage(): String {
|
||||
val actionMessage = VkUtils.getActionConversationText(
|
||||
message = lastMessage,
|
||||
youPrefix = "You",
|
||||
messageUser = lastMessage?.user,
|
||||
messageGroup = lastMessage?.group,
|
||||
action = lastMessage?.getPreparedAction(),
|
||||
actionUser = lastMessage?.actionUser,
|
||||
actionGroup = lastMessage?.actionGroup
|
||||
)
|
||||
|
||||
val attachmentIcon: UiImage? = when {
|
||||
lastMessage?.text == null -> null
|
||||
!lastMessage?.forwards.isNullOrEmpty() -> {
|
||||
if (lastMessage?.forwards?.size == 1) {
|
||||
UiImage.Resource(R.drawable.ic_attachment_forwarded_message)
|
||||
} else {
|
||||
UiImage.Resource(R.drawable.ic_attachment_forwarded_messages)
|
||||
}
|
||||
}
|
||||
|
||||
else -> VkUtils.getAttachmentConversationIcon(lastMessage)
|
||||
}
|
||||
|
||||
val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText(
|
||||
message = lastMessage
|
||||
) else null)
|
||||
|
||||
val forwardsMessage = (if (lastMessage?.text == null) VkUtils.getForwardsText(
|
||||
message = lastMessage
|
||||
) else null)
|
||||
|
||||
val messageText = lastMessage?.text?.let(UiText::Simple)
|
||||
|
||||
var prefix = when {
|
||||
actionMessage != null -> ""
|
||||
lastMessage?.isOut.isTrue -> "You: "
|
||||
else ->
|
||||
when {
|
||||
lastMessage?.user != null && lastMessage?.user?.firstName?.isNotBlank().isTrue -> {
|
||||
"${lastMessage?.user?.firstName}: "
|
||||
}
|
||||
|
||||
lastMessage?.group != null && lastMessage?.group?.name?.isNotBlank().isTrue -> {
|
||||
"${lastMessage?.group?.name}: "
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
if ((!peerType.isChat() && lastMessage?.isOut.isFalse) || id == UserConfig.userId)
|
||||
prefix = ""
|
||||
|
||||
val finalText =
|
||||
(actionMessage ?: forwardsMessage ?: attachmentText ?: messageText)
|
||||
?.parseString(AppGlobal.Instance)
|
||||
?.let(VkUtils::prepareMessageText)
|
||||
?.let { text -> "$prefix$text" }
|
||||
|
||||
|
||||
return finalText.orDots()
|
||||
}
|
||||
|
||||
fun extractAttachmentImage(): UiImage? {
|
||||
if (lastMessage?.text == null) return null
|
||||
return VkUtils.getAttachmentConversationIcon(lastMessage)
|
||||
}
|
||||
|
||||
fun extractReadCondition(): Boolean {
|
||||
return (lastMessage?.isOut.isTrue && isOutUnread()) ||
|
||||
(lastMessage?.isOut.isFalse && isInUnread())
|
||||
}
|
||||
|
||||
fun extractDate(): String {
|
||||
return TimeUtils.getLocalizedTime(AppGlobal.Instance, (lastMessage?.date ?: -1) * 1000L)
|
||||
}
|
||||
|
||||
// TODO: 05.08.2023, Danil Nikolaev: rewrite
|
||||
fun extractBirthday(): Boolean {
|
||||
val birthday = conversationUser?.birthday ?: return false
|
||||
val splitBirthday = birthday.split(".")
|
||||
|
||||
return if (splitBirthday.size > 1) {
|
||||
val birthdayCalendar = Calendar.getInstance().apply {
|
||||
this[Calendar.DAY_OF_MONTH] = splitBirthday.first().toIntOrNull() ?: -1
|
||||
this[Calendar.MONTH] = (splitBirthday[1].toIntOrNull() ?: 0) - 1
|
||||
}
|
||||
val nowCalendar = Calendar.getInstance()
|
||||
|
||||
(nowCalendar[Calendar.DAY_OF_MONTH] == birthdayCalendar[Calendar.DAY_OF_MONTH]
|
||||
&& nowCalendar[Calendar.MONTH] == birthdayCalendar[Calendar.MONTH])
|
||||
} else false
|
||||
}
|
||||
|
||||
fun mapToPresentation() = VkConversationUi(
|
||||
conversationId = id,
|
||||
lastMessageId = lastMessageId,
|
||||
avatar = extractAvatar(),
|
||||
title = extractTitle(),
|
||||
unreadCount = extractUnreadCounterText(),
|
||||
date = extractDate(),
|
||||
message = extractMessage(),
|
||||
attachmentImage = extractAttachmentImage(),
|
||||
isPinned = majorId > 0,
|
||||
actionState = ActionState.parse(isPhantom, isCallInProgress),
|
||||
isBirthday = extractBirthday(),
|
||||
isUnread = extractReadCondition(),
|
||||
isAccount = isAccount(),
|
||||
isOnline = !isAccount() && conversationUser?.online == true,
|
||||
lastMessage = lastMessage,
|
||||
conversationUser = conversationUser,
|
||||
conversationGroup = conversationGroup,
|
||||
peerType = peerType
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.meloda.fast.api.model.presentation
|
||||
|
||||
import com.meloda.fast.api.model.ActionState
|
||||
import com.meloda.fast.api.model.ConversationPeerType
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.model.base.AdapterDiffItem
|
||||
import com.meloda.fast.model.base.UiImage
|
||||
import com.meloda.fast.model.base.UiText
|
||||
|
||||
data class VkConversationUi(
|
||||
val conversationId: Int,
|
||||
val lastMessageId: Int,
|
||||
val avatar: UiImage,
|
||||
val title: UiText,
|
||||
val unreadCount: String?,
|
||||
val date: String,
|
||||
val message: String,
|
||||
val attachmentImage: UiImage?,
|
||||
val isPinned: Boolean,
|
||||
val actionState: ActionState,
|
||||
val isBirthday: Boolean,
|
||||
val isUnread: Boolean,
|
||||
val isAccount: Boolean,
|
||||
val isOnline: Boolean,
|
||||
val lastMessage: VkMessage?,
|
||||
val conversationUser: VkUser?,
|
||||
val conversationGroup: VkGroup?,
|
||||
val peerType: ConversationPeerType,
|
||||
) : AdapterDiffItem {
|
||||
override val id = conversationId
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.api.base.ApiError
|
||||
|
||||
@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 VkErrors {
|
||||
const val Unknown = "unknown_error"
|
||||
|
||||
const val NeedValidation = "need_validation"
|
||||
const val NeedCaptcha = "need_captcha"
|
||||
const val InvalidRequest = "invalid_request"
|
||||
|
||||
}
|
||||
|
||||
object VkErrorTypes {
|
||||
const val OtpFormatIncorrect = "otp_format_is_incorrect"
|
||||
const val WrongOtp = "wrong_otp"
|
||||
}
|
||||
|
||||
object VkErrorMessages {
|
||||
const val UserBanned = "user has been banned"
|
||||
}
|
||||
|
||||
open class AuthorizationError : ApiError()
|
||||
|
||||
class TokenExpiredError : AuthorizationError()
|
||||
|
||||
data class ValidationRequiredError(
|
||||
@SerializedName("validation_type")
|
||||
val validationType: String,
|
||||
@SerializedName("validation_sid")
|
||||
val validationSid: String,
|
||||
@SerializedName("phone_mask")
|
||||
val phoneMask: String,
|
||||
@SerializedName("redirect_uri")
|
||||
val redirectUri: String,
|
||||
@SerializedName("validation_resend")
|
||||
val validationResend: String
|
||||
) : ApiError()
|
||||
|
||||
data class CaptchaRequiredError(
|
||||
@SerializedName("captcha_sid")
|
||||
val captchaSid: String,
|
||||
@SerializedName("captcha_img")
|
||||
val captchaImg: String
|
||||
) : ApiError()
|
||||
|
||||
object WrongTwoFaCodeFormatError : ApiError()
|
||||
|
||||
object WrongTwoFaCodeError : ApiError()
|
||||
|
||||
data class UserBannedError(
|
||||
@SerializedName("ban_info")
|
||||
val banInfo: BanInfo
|
||||
) : ApiError() {
|
||||
|
||||
data class BanInfo(
|
||||
@SerializedName("member_name")
|
||||
val memberName: String,
|
||||
val message: String,
|
||||
@SerializedName("access_token")
|
||||
val accessToken: String,
|
||||
@SerializedName("restore_url")
|
||||
val restoreUrl: String
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.network.account.AccountUrls
|
||||
import com.meloda.fast.api.network.ota.OtaUrls
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.net.URLEncoder
|
||||
|
||||
class AuthInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val builder = chain.request().url.newBuilder()
|
||||
|
||||
val url = builder.build().toUrl().toString()
|
||||
|
||||
if (!url.contains("upload.php") && !url.contains(OtaUrls.GetActualUrl)) {
|
||||
builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
|
||||
}
|
||||
|
||||
if (!url.contains(AccountUrls.SetOnline) && !url.contains("upload.php")) {
|
||||
UserConfig.accessToken.let {
|
||||
if (it.isNotBlank())
|
||||
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.base.ApiError
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.*
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
class ResultCallFactory(private val gson: Gson) : CallAdapter.Factory() {
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit,
|
||||
): CallAdapter<*, *>? {
|
||||
val rawReturnType: Class<*> = getRawType(returnType)
|
||||
if (rawReturnType == Call::class.java) {
|
||||
if (returnType is ParameterizedType) {
|
||||
val callInnerType: Type = getParameterUpperBound(0, returnType)
|
||||
if (getRawType(callInnerType) == ApiAnswer::class.java) {
|
||||
if (callInnerType is ParameterizedType) {
|
||||
val resultInnerType = getParameterUpperBound(0, callInnerType)
|
||||
return ResultCallAdapter<Any?>(resultInnerType, gson)
|
||||
}
|
||||
return ResultCallAdapter<Nothing>(Nothing::class.java, gson)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class CallDelegate<In, Out>(protected val proxy: Call<In>) : Call<Out> {
|
||||
|
||||
override fun execute(): Response<Out> = throw NotImplementedError()
|
||||
|
||||
final override fun enqueue(callback: Callback<Out>) = enqueueImpl(callback)
|
||||
|
||||
final override fun clone(): Call<Out> = cloneImpl()
|
||||
|
||||
override fun cancel() = proxy.cancel()
|
||||
|
||||
override fun request(): Request = proxy.request()
|
||||
|
||||
override fun isExecuted() = proxy.isExecuted
|
||||
|
||||
override fun isCanceled() = proxy.isCanceled
|
||||
|
||||
abstract fun enqueueImpl(callback: Callback<Out>)
|
||||
|
||||
abstract fun cloneImpl(): Call<Out>
|
||||
}
|
||||
|
||||
private class ResultCallAdapter<R>(private val type: Type, private val gson: Gson) : CallAdapter<R, Call<ApiAnswer<R>>> {
|
||||
|
||||
override fun responseType() = type
|
||||
|
||||
override fun adapt(call: Call<R>): Call<ApiAnswer<R>> = ResultCall(call, gson)
|
||||
}
|
||||
|
||||
internal class ResultCall<T>(proxy: Call<T>, private val gson: Gson) : CallDelegate<T, ApiAnswer<T>>(proxy) {
|
||||
|
||||
override fun enqueueImpl(callback: Callback<ApiAnswer<T>>) {
|
||||
proxy.enqueue(ResultCallback(this, callback, gson))
|
||||
}
|
||||
|
||||
override fun cloneImpl(): ResultCall<T> {
|
||||
return ResultCall(proxy.clone(), gson)
|
||||
}
|
||||
|
||||
private class ResultCallback<T>(
|
||||
private val proxy: ResultCall<T>,
|
||||
private val callback: Callback<ApiAnswer<T>>,
|
||||
private val gson: Gson
|
||||
) : Callback<T> {
|
||||
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val result: ApiAnswer<T> =
|
||||
if (response.isSuccessful) {
|
||||
val baseBody = response.body()
|
||||
if (baseBody !is ApiResponse<*>) {
|
||||
ApiAnswer.Success(baseBody as T)
|
||||
} else {
|
||||
val body = baseBody as? ApiResponse<*>
|
||||
if (body?.error != null) {
|
||||
VkUtils.getApiError(gson, gson.toJson(body.error))
|
||||
} else {
|
||||
ApiAnswer.Success(body as T)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val errorBodyString = response.errorBody()?.string()
|
||||
|
||||
VkUtils.getApiError(gson, errorBodyString)
|
||||
}
|
||||
|
||||
if (checkErrors(call, result)) {
|
||||
return
|
||||
}
|
||||
|
||||
callback.onResponse(proxy, Response.success(result))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, error: Throwable) {
|
||||
callback.onResponse(
|
||||
proxy,
|
||||
Response.success(ApiAnswer.Error(ApiError(throwable = error)))
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkErrors(call: Call<T>, result: ApiAnswer<*>): Boolean {
|
||||
if (result.isError()) {
|
||||
result.error.throwable?.run {
|
||||
onFailure(call, this)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun timeout(): Timeout {
|
||||
return proxy.timeout()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ApiAnswer<out R> {
|
||||
|
||||
data class Success<out T>(val data: T) : ApiAnswer<T>()
|
||||
data class Error(val error: ApiError) : ApiAnswer<Nothing>()
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun isSuccessful(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@ApiAnswer is Success)
|
||||
}
|
||||
return this is Success
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun isError(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@ApiAnswer is Error)
|
||||
}
|
||||
return this is Error
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
object VkUrls {
|
||||
|
||||
const val OAUTH = "https://oauth.vk.com"
|
||||
const val API = "https://api.vk.com/method"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.meloda.fast.api.network.account
|
||||
|
||||
import com.meloda.fast.api.ApiExtensions.intString
|
||||
|
||||
data class AccountSetOnlineRequest(
|
||||
val voip: Boolean,
|
||||
val accessToken: String
|
||||
) {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"voip" to voip.intString,
|
||||
"access_token" to accessToken
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
data class AccountSetOfflineRequest(val accessToken: String) {
|
||||
val map get() = mutableMapOf("access_token" to accessToken)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.meloda.fast.api.network.account
|
||||
|
||||
import com.meloda.fast.api.network.VkUrls
|
||||
|
||||
object AccountUrls {
|
||||
|
||||
const val SetOnline = "${VkUrls.API}/account.setOnline"
|
||||
const val SetOffline = "${VkUrls.API}/account.setOffline"
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package com.meloda.fast.api.network.audio
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.meloda.fast.api.network.audio
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AudiosGetUploadServerResponse(
|
||||
@SerializedName("upload_url")
|
||||
val uploadUrl: String
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class AudiosUploadResponse(
|
||||
val redirect: String,
|
||||
val server: Int,
|
||||
val audio: String?,
|
||||
val hash: String,
|
||||
val error: String?
|
||||
) : Parcelable
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.meloda.fast.api.network.audio
|
||||
|
||||
import com.meloda.fast.api.network.VkUrls
|
||||
|
||||
object AudiosUrls {
|
||||
|
||||
const val GetUploadServer = "${VkUrls.API}/audio.getUploadServer"
|
||||
|
||||
const val Save = "${VkUrls.API}/audio.save"
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.meloda.fast.api.network.auth
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AuthDirectRequest(
|
||||
val grantType: String,
|
||||
val clientId: String,
|
||||
val clientSecret: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val scope: String,
|
||||
val twoFaSupported: Boolean = true,
|
||||
val twoFaForceSms: Boolean = false,
|
||||
val twoFaCode: String? = null,
|
||||
val captchaSid: String? = null,
|
||||
val captchaKey: String? = null,
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"grant_type" to grantType,
|
||||
"client_id" to clientId,
|
||||
"client_secret" to clientSecret,
|
||||
"username" to username,
|
||||
"password" to password,
|
||||
"scope" to scope,
|
||||
"2fa_supported" to if (twoFaSupported) "1" else "0",
|
||||
"force_sms" to if (twoFaForceSms) "1" else "0"
|
||||
)
|
||||
.apply {
|
||||
twoFaCode?.let { this["code"] = it }
|
||||
captchaSid?.let { this["captcha_sid"] = it }
|
||||
captchaKey?.let { this["captcha_key"] = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AuthWithAppRequest(
|
||||
val redirectUrl: String = "https://oauth.vk.com/blank.html",
|
||||
val display: String = "page",
|
||||
val responseType: String = "token",
|
||||
val accessToken: String,
|
||||
val revoke: Int = 1,
|
||||
val scope: Int = 136297695,
|
||||
val clientId: String = VKConstants.FAST_APP_ID,
|
||||
val sdkPackage: String = BuildConfig.sdkPackage,
|
||||
val sdkFingerprint: String = BuildConfig.sdkFingerprint
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"redirect_url" to redirectUrl,
|
||||
"display" to display,
|
||||
"response_type" to responseType,
|
||||
"access_token" to accessToken,
|
||||
"revoke" to revoke.toString(),
|
||||
"scope" to scope.toString(),
|
||||
"client_id" to clientId,
|
||||
"sdk_package" to sdkPackage,
|
||||
"sdk_fingerprint" to sdkFingerprint
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.network.auth
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AuthDirectResponse(
|
||||
@SerializedName("access_token") val accessToken: String?,
|
||||
@SerializedName("user_id") val userId: Int?,
|
||||
@SerializedName("trusted_hash") val twoFaHash: String?,
|
||||
@SerializedName("validation_sid") val validationSid: String?,
|
||||
@SerializedName("validation_type") val validationType: String?,
|
||||
@SerializedName("phone_mask") val phoneMask: String?,
|
||||
@SerializedName("redirect_uri") val redirectUrl: String?,
|
||||
@SerializedName("validation_resend") val validationResend: String?,
|
||||
@SerializedName("cant_get_code_open_restore") val isCanNotGetCodeNeedToOpenRestore: Boolean
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class SendSmsResponse(
|
||||
@SerializedName("sid") val validationSid: String?,
|
||||
@SerializedName("delay") val delay: Int?,
|
||||
@SerializedName("validation_type") val validationType: String?,
|
||||
@SerializedName("validation_resend") val validationResend: String?
|
||||
) : Parcelable
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.meloda.fast.api.network.auth
|
||||
|
||||
import com.meloda.fast.api.network.VkUrls
|
||||
|
||||
object AuthUrls {
|
||||
|
||||
const val DirectAuth = "${VkUrls.OAUTH}/token"
|
||||
const val SendSms = "${VkUrls.API}/auth.validatePhone"
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.meloda.fast.api.network.conversations
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ConversationsGetRequest(
|
||||
val count: Int? = null,
|
||||
val offset: Int? = null,
|
||||
val fields: String = "",
|
||||
val filter: String = "all",
|
||||
val extended: Boolean? = true,
|
||||
val startMessageId: Int? = null
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"fields" to fields,
|
||||
"filter" to filter
|
||||
).apply {
|
||||
count?.let { this["count"] = it.toString() }
|
||||
offset?.let { this["offset"] = it.toString() }
|
||||
extended?.let { this["extended"] = it.toString() }
|
||||
startMessageId?.let { this["start_message_id"] = it.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ConversationsDeleteRequest(val peerId: Int) : Parcelable {
|
||||
val map get() = mapOf("peer_id" to peerId.toString())
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ConversationsPinRequest(val peerId: Int) : Parcelable {
|
||||
val map get() = mapOf("peer_id" to peerId.toString())
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ConversationsUnpinRequest(val peerId: Int) : Parcelable {
|
||||
val map get() = mapOf("peer_id" to peerId.toString())
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.network.conversations
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.api.model.data.BaseVkConversation
|
||||
import com.meloda.fast.api.model.base.BaseVkGroup
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
import com.meloda.fast.api.model.base.BaseVkUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ConversationsGetResponse(
|
||||
val count: Int,
|
||||
val items: List<ConversationsResponseItems>,
|
||||
@SerializedName("unread_count")
|
||||
val unreadCount: Int?,
|
||||
val profiles: List<BaseVkUser>?,
|
||||
val groups: List<BaseVkGroup>?
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class ConversationsResponseItems(
|
||||
val conversation: BaseVkConversation,
|
||||
@SerializedName("last_message")
|
||||
val lastMessage: BaseVkMessage?
|
||||
) : Parcelable
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.meloda.fast.api.network.conversations
|
||||
|
||||
import com.meloda.fast.api.network.VkUrls
|
||||
|
||||
object ConversationsUrls {
|
||||
|
||||
const val Get = "${VkUrls.API}/messages.getConversations"
|
||||
const val Delete = "${VkUrls.API}/messages.deleteConversation"
|
||||
const val Pin = "${VkUrls.API}/messages.pinConversation"
|
||||
const val Unpin = "${VkUrls.API}/messages.unpinConversation"
|
||||
const val ReorderPinned = "${VkUrls.API}/messages.reorderPinnedConversations"
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package com.meloda.fast.api.network.files
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.meloda.fast.api.network.files
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkFile
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkVoiceMessage
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class FilesGetMessagesUploadServerResponse(
|
||||
@SerializedName("upload_url")
|
||||
val uploadUrl: String
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class FilesUploadFileResponse(val file: String?, val error: String?) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class FilesSaveFileResponse(
|
||||
val type: String,
|
||||
@SerializedName("doc")
|
||||
val file: BaseVkFile?,
|
||||
@SerializedName("audio_message")
|
||||
val voiceMessage: BaseVkVoiceMessage?
|
||||
) : Parcelable
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.meloda.fast.api.network.files
|
||||
|
||||
import com.meloda.fast.api.network.VkUrls
|
||||
|
||||
object FilesUrls {
|
||||
|
||||
const val GetMessagesUploadServer = "${VkUrls.API}/docs.getMessagesUploadServer"
|
||||
|
||||
const val Save = "${VkUrls.API}/docs.save"
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.meloda.fast.api.network.longpoll
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class LongPollGetUpdatesRequest(
|
||||
val act: String = "a_check",
|
||||
val key: String,
|
||||
val ts: Int,
|
||||
val wait: Int,
|
||||
val mode: Int,
|
||||
val version: Int
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"act" to act,
|
||||
"key" to key,
|
||||
"ts" to ts.toString(),
|
||||
"wait" to wait.toString(),
|
||||
"mode" to mode.toString(),
|
||||
"version" to version.toString()
|
||||
)
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user