WIP
This commit is contained in:
parent
1807d6145f
commit
19b61c959b
6
.idea/assetWizardSettings.xml
generated
6
.idea/assetWizardSettings.xml
generated
@ -18,7 +18,7 @@
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/search/baseline_search_24.xml" />
|
||||
<entry key="url" value="file:$USER_HOME$/Android/Sdk/icons/material/materialicons/mic/baseline_mic_24.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
@ -28,8 +28,8 @@
|
||||
</option>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="outputName" value="baseline_search_24" />
|
||||
<entry key="sourceFile" value="C:\Users\erike\Downloads\comments-solid.svg" />
|
||||
<entry key="outputName" value="baseline_mic_24" />
|
||||
<entry key="sourceFile" value="$USER_HOME$/C:\Users\erike\Downloads\comments-solid.svg" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
|
||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.0" />
|
||||
</component>
|
||||
</project>
|
||||
242
.idea/navEditor.xml
generated
Normal file
242
.idea/navEditor.xml
generated
Normal file
@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="navEditor-manualLayoutAlgorithm2">
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="audio_recorder_navigation.xml">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="audioDraftsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="130" />
|
||||
<option name="y" value="18" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="audioRecorderMainFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="-74" />
|
||||
<option name="y" value="17" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="home_navigation.xml">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="feedFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="-280" />
|
||||
<option name="y" value="-77" />
|
||||
</Point>
|
||||
</option>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="action_feedFragment_to_profileActivity">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="notificationsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="-282" />
|
||||
<option name="y" value="-368" />
|
||||
</Point>
|
||||
</option>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="action_notificationsFragment_to_profileActivity">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="profileActivity">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="31" />
|
||||
<option name="y" value="-367" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="searchFragment2">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="-281" />
|
||||
<option name="y" value="221" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="profile_navigation.xml">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="audiosFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="256" />
|
||||
<option name="y" value="12" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="discussionsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="12" />
|
||||
<option name="y" value="12" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="imagesFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="12" />
|
||||
<option name="y" value="368" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="settings_navigation.xml">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="changePasswordFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="256" />
|
||||
<option name="y" value="368" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="feedSettingsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="256" />
|
||||
<option name="y" value="12" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="sessionsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="256" />
|
||||
<option name="y" value="724" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="settingsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="12" />
|
||||
<option name="y" value="76" />
|
||||
</Point>
|
||||
</option>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="action_settingsFragment_to_changePasswordFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="action_settingsFragment_to_feedSettingsFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="action_settingsFragment_to_sessionsFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -4,6 +4,7 @@ plugins {
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.dagger.hilt.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
|
||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -89,6 +90,19 @@ dependencies {
|
||||
implementation "io.noties.markwon:editor:$markwon_version"
|
||||
implementation "io.noties.markwon:image-picasso:$markwon_version"
|
||||
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||
|
||||
|
||||
|
||||
implementation ('io.socket:socket.io-client:2.1.0') {
|
||||
// excluding org.json which is provided by Android
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
def room_version = "2.5.2"
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
|
||||
}
|
||||
|
||||
kapt {
|
||||
|
||||
@ -11,11 +11,12 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Isolaatti"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Isolaatti.Splash" android:noHistory="true">
|
||||
android:theme="@style/Theme.Isolaatti.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package com.isolaatti
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import com.isolaatti.connectivity.SocketIO
|
||||
|
||||
class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
var startedActivitiesCount = 0
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
|
||||
startedActivitiesCount++
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
startedActivitiesCount--
|
||||
|
||||
if(startedActivitiesCount == 0) {
|
||||
SocketIO.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
package com.isolaatti
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.isolaatti.auth.data.AuthRepositoryImpl
|
||||
import com.isolaatti.home.HomeActivity
|
||||
@ -15,6 +17,14 @@ class MainActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var authRepository: AuthRepositoryImpl
|
||||
|
||||
private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
|
||||
if(activityResult.resultCode == Activity.RESULT_OK) {
|
||||
val homeActivityIntent = Intent(this@MainActivity, HomeActivity::class.java)
|
||||
homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(homeActivityIntent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var isLoading = true
|
||||
val splashScreen = installSplashScreen()
|
||||
@ -25,11 +35,12 @@ class MainActivity : ComponentActivity() {
|
||||
val currentToken = authRepository.getCurrentToken()
|
||||
|
||||
if(currentToken == null) {
|
||||
startActivity(Intent(this@MainActivity, LogInActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
})
|
||||
|
||||
signInActivityResult.launch(Intent(this@MainActivity, LogInActivity::class.java))
|
||||
} else {
|
||||
startActivity(Intent(this@MainActivity, HomeActivity::class.java))
|
||||
val homeActivityIntent = Intent(this@MainActivity, HomeActivity::class.java)
|
||||
homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(homeActivityIntent)
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.isolaatti
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import com.isolaatti.auth.data.AuthRepositoryImpl
|
||||
import com.isolaatti.auth.data.local.TokenStorage
|
||||
import com.isolaatti.auth.data.remote.AuthApi
|
||||
@ -13,7 +15,15 @@ import javax.inject.Singleton
|
||||
@HiltAndroidApp
|
||||
class MyApplication : Application() {
|
||||
|
||||
private val activityLifecycleCallbacks = ActivityLifecycleCallbacks()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.isolaatti.audio.recorder.ui
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
class AudioDraftsFragment : Fragment() {
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.isolaatti.audio.recorder.ui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
class AudioRecorderActivity : ComponentActivity() {
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.isolaatti.audio.recorder.ui
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
class AudioRecorderMainFragment : Fragment() {
|
||||
}
|
||||
@ -1,18 +1,16 @@
|
||||
package com.isolaatti.auth.data
|
||||
|
||||
import android.util.Log
|
||||
import com.isolaatti.auth.data.remote.AuthTokenDto
|
||||
import com.isolaatti.auth.data.local.TokenStorage
|
||||
import com.isolaatti.auth.data.remote.AuthApi
|
||||
import com.isolaatti.auth.data.remote.Credential
|
||||
import com.isolaatti.auth.domain.AuthRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.await
|
||||
import retrofit2.awaitResponse
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@ -20,22 +18,41 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
private val tokenStorage: TokenStorage,
|
||||
private val authApi: AuthApi
|
||||
) : AuthRepository {
|
||||
override fun authWithEmailAndPassword(email: String, password: String): Flow<Boolean> = flow {
|
||||
val res = authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse()
|
||||
val authDto = res.body()
|
||||
override fun authWithEmailAndPassword(
|
||||
email: String,
|
||||
password: String
|
||||
): Flow<Resource<Boolean>> = flow {
|
||||
try {
|
||||
val res =
|
||||
authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse()
|
||||
|
||||
if(res.isSuccessful) {
|
||||
val dto = res.body()
|
||||
if(dto == null) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
||||
return@flow
|
||||
}
|
||||
tokenStorage.storeToken(dto)
|
||||
emit(Resource.Success(true))
|
||||
return@flow
|
||||
}
|
||||
|
||||
if(authDto != null) {
|
||||
tokenStorage.storeToken(authDto)
|
||||
emit(true)
|
||||
} else {
|
||||
emit(false)
|
||||
when(res.code()){
|
||||
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
|
||||
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
|
||||
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
||||
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout(): Flow<Boolean> = flow {
|
||||
tokenStorage.removeToken()
|
||||
authApi.signOut()
|
||||
try {
|
||||
authApi.signOut().awaitResponse()
|
||||
} catch(_: Exception) { }
|
||||
emit(true)
|
||||
}
|
||||
|
||||
|
||||
@ -79,7 +79,9 @@ class TokenStorage @Inject constructor(@ApplicationContext private val applicati
|
||||
|
||||
fun removeToken() {
|
||||
if(file.exists()) {
|
||||
try {
|
||||
file.delete()
|
||||
} catch(_: SecurityException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package com.isolaatti.auth.data.remote
|
||||
|
||||
data class AuthTokenDto(val expires: String, val created: String, val token: String) {
|
||||
data class AuthTokenDto(val token: String) {
|
||||
override fun toString(): String {
|
||||
return token
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package com.isolaatti.auth.domain
|
||||
|
||||
import com.isolaatti.auth.data.remote.AuthTokenDto
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AuthRepository {
|
||||
fun authWithEmailAndPassword(email: String, password: String): Flow<Boolean>
|
||||
fun authWithEmailAndPassword(email: String, password: String): Flow<Resource<Boolean>>
|
||||
fun logout(): Flow<Boolean>
|
||||
fun getCurrentToken(): AuthTokenDto?
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.isolaatti.common
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.isolaatti.utils.Resource
|
||||
|
||||
class ErrorMessageViewModel : ViewModel() {
|
||||
val error: MutableLiveData<Resource.Error.ErrorType> = MutableLiveData()
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.isolaatti.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.home.HomeActivity
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class IsolaattiBaseActivity : AppCompatActivity() {
|
||||
|
||||
val errorViewModel: ErrorMessageViewModel by viewModels()
|
||||
|
||||
private val errorObserver: Observer<Resource.Error.ErrorType> = Observer {
|
||||
when(it) {
|
||||
Resource.Error.ErrorType.AuthError -> showReAuthDialog()
|
||||
Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage()
|
||||
Resource.Error.ErrorType.NotFoundError -> showNotFoundErrorMessage()
|
||||
Resource.Error.ErrorType.ServerError -> showServerErrorMessage()
|
||||
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
|
||||
if(activityResult.resultCode == Activity.RESULT_OK) {
|
||||
onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when a refresh should be performed. For example,
|
||||
* when sign in flow is started completed from here, it is needed to know
|
||||
* when it is complete.
|
||||
*/
|
||||
abstract fun onRetry()
|
||||
|
||||
private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ ->
|
||||
|
||||
}
|
||||
|
||||
private fun showReAuthDialog() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.need_reauth_message)
|
||||
.setPositiveButton(R.string.accept, onAcceptReAuthClick)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showNetworkErrorMessage() {
|
||||
Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun showServerErrorMessage() {
|
||||
|
||||
}
|
||||
|
||||
private fun showNotFoundErrorMessage() {
|
||||
|
||||
}
|
||||
|
||||
private fun showUnknownErrorMessage() {
|
||||
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
errorViewModel.error.observe(this, errorObserver)
|
||||
Log.d("IsolaattiBaseActivity", errorViewModel.toString())
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package com.isolaatti.connectivity
|
||||
|
||||
import com.isolaatti.BuildConfig
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
@ -14,7 +15,7 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor:
|
||||
val excludedUrlsFromAuthentication = listOf(
|
||||
"/api/LogIn"
|
||||
)
|
||||
const val BASE_URL = "https://isolaatti.com/api/"
|
||||
const val BASE_URL = "${BuildConfig.backend}/api/"
|
||||
}
|
||||
|
||||
private val okHttpClient get() = OkHttpClient.Builder()
|
||||
|
||||
26
app/src/main/java/com/isolaatti/connectivity/SocketIO.kt
Normal file
26
app/src/main/java/com/isolaatti/connectivity/SocketIO.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package com.isolaatti.connectivity
|
||||
|
||||
import io.socket.client.IO
|
||||
import io.socket.client.Socket
|
||||
|
||||
object SocketIO {
|
||||
private lateinit var socket: Socket
|
||||
val instance: Socket get() {
|
||||
return if(this::socket.isInitialized) {
|
||||
if(socket.connected()) {
|
||||
socket
|
||||
} else {
|
||||
IO.socket("")
|
||||
}
|
||||
} else {
|
||||
socket = IO.socket("")
|
||||
socket
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
if(this::socket.isInitialized) {
|
||||
socket.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
package com.isolaatti.feed.data.remote
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface FeedApi {
|
||||
@GET("Feed")
|
||||
fun fetchFeed(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
package com.isolaatti.feed.data.remote
|
||||
|
||||
data class FeedDto(
|
||||
val data: MutableList<PostDto>,
|
||||
val moreContent: Boolean
|
||||
)
|
||||
@ -1,12 +0,0 @@
|
||||
package com.isolaatti.feed.data.remote
|
||||
|
||||
import com.isolaatti.feed.domain.model.Post
|
||||
|
||||
data class PostDto(
|
||||
val post: Post,
|
||||
var numberOfLikes: Int,
|
||||
var numberOfComments: Int,
|
||||
val userName: String,
|
||||
val squadName: String?,
|
||||
var liked: Boolean
|
||||
)
|
||||
@ -1,20 +0,0 @@
|
||||
package com.isolaatti.feed.data.repository
|
||||
|
||||
import com.isolaatti.feed.data.remote.FeedApi
|
||||
import com.isolaatti.feed.data.remote.FeedDto
|
||||
import com.isolaatti.feed.domain.repository.FeedRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import retrofit2.awaitResponse
|
||||
import javax.inject.Inject
|
||||
|
||||
class FeedRepositoryImpl @Inject constructor(private val feedApi: FeedApi): FeedRepository {
|
||||
override fun getNextPage(lastId: Long, count: Int): Flow<FeedDto?> = flow {
|
||||
val response = feedApi.fetchFeed(lastId, count).awaitResponse().body()
|
||||
if(response != null){
|
||||
emit(response)
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package com.isolaatti.feed.domain.model
|
||||
|
||||
data class Post(
|
||||
val id: Long,
|
||||
var textContent: String,
|
||||
val userId: Int,
|
||||
val privacy: Int,
|
||||
val date: String,
|
||||
var audioId: String,
|
||||
val squadId: String,
|
||||
val linkedDiscussionId: Long,
|
||||
val linkedCommentId: Long
|
||||
)
|
||||
@ -1,8 +0,0 @@
|
||||
package com.isolaatti.feed.domain.repository
|
||||
|
||||
import com.isolaatti.feed.data.remote.FeedDto
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface FeedRepository {
|
||||
fun getNextPage(lastId: Long, count: Int): Flow<FeedDto?>
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.isolaatti.feed.ui
|
||||
package com.isolaatti.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@ -10,6 +10,7 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
import com.isolaatti.databinding.FragmentFeedBinding
|
||||
import com.isolaatti.posting.posts.presentation.PostsViewModel
|
||||
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
|
||||
@ -34,6 +35,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
}
|
||||
|
||||
private val viewModel: PostsViewModel by activityViewModels()
|
||||
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
|
||||
private lateinit var viewBinding: FragmentFeedBinding
|
||||
private lateinit var adapter: PostsRecyclerViewAdapter
|
||||
|
||||
@ -67,6 +69,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val markwon = Markwon.builder(requireContext())
|
||||
.usePlugin(object: AbstractMarkwonPlugin() {
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
@ -78,20 +81,32 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.build()
|
||||
adapter = PostsRecyclerViewAdapter(markwon, this, listOf())
|
||||
adapter = PostsRecyclerViewAdapter(markwon, this, null)
|
||||
viewBinding.feedRecyclerView.adapter = adapter
|
||||
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
|
||||
viewModel.posts.observe(viewLifecycleOwner){
|
||||
Log.d("recycler", it.data.toString())
|
||||
adapter.updateList(it.data.toList(),null)
|
||||
adapter.updateList(it,null)
|
||||
}
|
||||
Log.d("FeedFragment", errorViewModel.toString())
|
||||
viewModel.errorLoading.observe(viewLifecycleOwner) {
|
||||
errorViewModel.error.postValue(it)
|
||||
Log.d("FeedFragment", it.toString())
|
||||
}
|
||||
viewModel.postLiked.observe(viewLifecycleOwner) {
|
||||
adapter.updateList(viewModel.posts.value?.data, PostsRecyclerViewAdapter.UpdateEvent(
|
||||
viewModel.posts.value?.let { feed ->
|
||||
adapter.updateList(
|
||||
feed, PostsRecyclerViewAdapter.UpdateEvent(
|
||||
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onNewMenuItemClicked(v: View) {
|
||||
|
||||
}
|
||||
|
||||
override fun onLiked(postId: Long) = viewModel.likePost(postId)
|
||||
|
||||
@ -4,17 +4,24 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
import com.isolaatti.databinding.ActivityHomeBinding
|
||||
import com.isolaatti.posting.posts.presentation.PostsViewModel
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeActivity : AppCompatActivity() {
|
||||
lateinit var viewBinding: ActivityHomeBinding
|
||||
val postsViewModel: PostsViewModel by viewModels()
|
||||
class HomeActivity : IsolaattiBaseActivity() {
|
||||
private lateinit var viewBinding: ActivityHomeBinding
|
||||
private val postsViewModel: PostsViewModel by viewModels()
|
||||
override fun onRetry() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -33,4 +40,8 @@ class HomeActivity : AppCompatActivity() {
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package com.isolaatti.home
|
||||
|
||||
import com.isolaatti.connectivity.RetrofitClient
|
||||
import com.isolaatti.feed.data.remote.FeedApi
|
||||
import com.isolaatti.feed.data.repository.FeedRepositoryImpl
|
||||
import com.isolaatti.feed.domain.repository.FeedRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class Module {
|
||||
@Provides
|
||||
fun provideFeedApi(retrofitClient: RetrofitClient): FeedApi {
|
||||
return retrofitClient.client.create(FeedApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideFeedRepository(feedApi: FeedApi): FeedRepository {
|
||||
return FeedRepositoryImpl(feedApi)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.isolaatti.home.search.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.isolaatti.databinding.FragmentSearchBinding
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
|
||||
lateinit var viewBinding: FragmentSearchBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
viewBinding = FragmentSearchBinding.inflate(inflater)
|
||||
return viewBinding.root
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,31 @@
|
||||
package com.isolaatti.login
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.BuildConfig
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
import com.isolaatti.databinding.ActivityLoginBinding
|
||||
import com.isolaatti.home.HomeActivity
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LogInActivity: AppCompatActivity() {
|
||||
|
||||
|
||||
lateinit var viewBinding: ActivityLoginBinding
|
||||
val viewModel: LogInViewModel by viewModels()
|
||||
private val viewModel: LogInViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -23,11 +34,33 @@ class LogInActivity: AppCompatActivity() {
|
||||
setContentView(viewBinding.root)
|
||||
|
||||
viewModel.signInSuccess.observe(this) {success ->
|
||||
if(success)
|
||||
startActivity(Intent(this, HomeActivity::class.java))
|
||||
else
|
||||
Toast.makeText(this,"Could not sign in, your credential is not correct...", Toast.LENGTH_SHORT).show()
|
||||
// Show login error message.
|
||||
if(success) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
viewModel.signInLoading.observe(this) {
|
||||
viewBinding.progressBar.visibility = View.VISIBLE
|
||||
viewBinding.signInBtn.isEnabled = false
|
||||
viewBinding.textFieldEmail.isEnabled = false
|
||||
viewBinding.textFieldPassword.isEnabled = false
|
||||
}
|
||||
viewModel.signInError.observe(this) {
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
viewBinding.signInBtn.isEnabled = true
|
||||
viewBinding.textFieldEmail.isEnabled = true
|
||||
viewBinding.textFieldPassword.isEnabled = true
|
||||
when(it) {
|
||||
Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage()
|
||||
Resource.Error.ErrorType.AuthError -> showWrongPasswordErrorMessage()
|
||||
Resource.Error.ErrorType.NotFoundError -> showNotFoundErrorMessage()
|
||||
Resource.Error.ErrorType.ServerError -> showServerErrorMessage()
|
||||
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
|
||||
null -> {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
viewModel.formIsValid.observe(this) {isValid ->
|
||||
@ -50,5 +83,42 @@ class LogInActivity: AppCompatActivity() {
|
||||
viewModel.signIn(email.toString(), password.toString())
|
||||
}
|
||||
|
||||
viewBinding.forgotPasswordBtn.setOnClickListener {
|
||||
openForgotPassword()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun openForgotPassword() {
|
||||
CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
.launchUrl(this, Uri.parse("${BuildConfig.backend}/recuperacion_cuenta"))
|
||||
}
|
||||
|
||||
private fun showWrongPasswordErrorMessage() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.wrong_password)
|
||||
.setNeutralButton(R.string.forgot_password) {_,_ -> openForgotPassword()}
|
||||
.setPositiveButton(R.string.dismiss, null)
|
||||
.show()
|
||||
}
|
||||
private fun showNetworkErrorMessage() {
|
||||
Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun showServerErrorMessage() {
|
||||
|
||||
}
|
||||
|
||||
private fun showNotFoundErrorMessage() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.account_not_found, viewBinding.textFieldEmail.editText?.text))
|
||||
.setPositiveButton(R.string.dismiss, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showUnknownErrorMessage() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -5,16 +5,22 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.auth.domain.AuthRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() {
|
||||
val signInSuccess: MutableLiveData<Boolean> = MutableLiveData()
|
||||
val signInError: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
|
||||
val signInLoading: MutableLiveData<Boolean> = MutableLiveData()
|
||||
val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
val emailUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
val passwordUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
@ -33,11 +39,23 @@ class LogInViewModel @Inject constructor(private val authRepository: AuthReposit
|
||||
}
|
||||
|
||||
fun signIn(email: String, password: String) {
|
||||
signInError.postValue(null)
|
||||
viewModelScope.launch {
|
||||
authRepository.authWithEmailAndPassword(email, password).collect {
|
||||
authRepository.authWithEmailAndPassword(email, password).onEach {
|
||||
Log.d("login", it.toString())
|
||||
signInSuccess.postValue(it)
|
||||
when(it) {
|
||||
is Resource.Success -> {
|
||||
signInLoading.postValue(false)
|
||||
signInSuccess.postValue(true)
|
||||
}
|
||||
is Resource.Error -> {
|
||||
signInLoading.postValue(false)
|
||||
signInError.postValue(it.errorType)
|
||||
}
|
||||
is Resource.Loading -> signInLoading.postValue(true)
|
||||
}
|
||||
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package com.isolaatti.posting
|
||||
|
||||
import com.isolaatti.connectivity.RetrofitClient
|
||||
import com.isolaatti.posting.posts.data.remote.PostsApi
|
||||
import com.isolaatti.posting.posts.data.remote.FeedsApi
|
||||
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
|
||||
import com.isolaatti.posting.posts.domain.PostsRepository
|
||||
import dagger.Module
|
||||
@ -13,12 +13,12 @@ import dagger.hilt.components.SingletonComponent
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class Module {
|
||||
@Provides
|
||||
fun providePostsApi(retrofitClient: RetrofitClient): PostsApi {
|
||||
return retrofitClient.client.create(PostsApi::class.java)
|
||||
fun providePostsApi(retrofitClient: RetrofitClient): FeedsApi {
|
||||
return retrofitClient.client.create(FeedsApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providePostsRepository(postsApi: PostsApi): PostsRepository {
|
||||
return PostsRepositoryImpl(postsApi)
|
||||
fun providePostsRepository(feedsApi: FeedsApi): PostsRepository {
|
||||
return PostsRepositoryImpl(feedsApi)
|
||||
}
|
||||
}
|
||||
@ -94,7 +94,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
|
||||
val textField = viewBinding.newCommentTextField
|
||||
|
||||
textField.setStartIconOnClickListener {
|
||||
AlertDialog.Builder(requireContext()).setView(R.layout.write_comment_multiline_dialog).show()
|
||||
|
||||
}
|
||||
|
||||
optionsViewModel.optionClicked(-1)
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
package com.isolaatti.posting.posts.data.remote
|
||||
|
||||
data class FeedDto(
|
||||
val data: MutableList<PostDto>,
|
||||
var moreContent: Boolean
|
||||
) {
|
||||
|
||||
fun concatFeed(otherFeed: FeedDto?): FeedDto {
|
||||
if(otherFeed != null) {
|
||||
data.addAll(otherFeed.data)
|
||||
moreContent = otherFeed.moreContent
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
data class PostDto(
|
||||
val post: Post,
|
||||
var numberOfLikes: Int,
|
||||
var numberOfComments: Int,
|
||||
val userName: String,
|
||||
val squadName: String?,
|
||||
var liked: Boolean
|
||||
) {
|
||||
data class Post(
|
||||
val id: Long,
|
||||
var textContent: String,
|
||||
val userId: Int,
|
||||
val privacy: Int,
|
||||
val date: String,
|
||||
var audioId: String,
|
||||
val squadId: String,
|
||||
val linkedDiscussionId: Long,
|
||||
val linkedCommentId: Long
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.isolaatti.posting.posts.data.remote
|
||||
|
||||
data class FeedFilterDto(
|
||||
val includeAudio: String,
|
||||
val includeFromSquads: String,
|
||||
val dataRange: DataRange
|
||||
) {
|
||||
data class DataRange(
|
||||
val enabled: Boolean,
|
||||
val from: String,
|
||||
val to: String
|
||||
)
|
||||
}
|
||||
@ -1,23 +1,25 @@
|
||||
package com.isolaatti.posting.posts.data.remote
|
||||
|
||||
import com.isolaatti.feed.data.remote.FeedDto
|
||||
import com.isolaatti.feed.data.remote.PostDto
|
||||
import com.isolaatti.profile.data.remote.ProfileListItemDto
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface PostsApi {
|
||||
interface FeedsApi {
|
||||
@GET("Fetch/PostsOfUser/{userId}")
|
||||
fun postsOfUser(@Path("userId") userId: Int,
|
||||
@Query("length") length: Int,
|
||||
@Query("lastId") lastId: Long,
|
||||
@Query("olderFirst") olderFirst: Boolean): Call<FeedDto>
|
||||
@Query("olderFirst") olderFirst: Boolean,
|
||||
@Query(value = "filterJson", encoded = false) filter: String): Call<FeedDto>
|
||||
|
||||
@GET("Fetch/Post/{postId}")
|
||||
fun getPost(@Path("postId") postId: Long): Call<PostDto>
|
||||
fun getPost(@Path("postId") postId: Long): Call<FeedDto>
|
||||
|
||||
@GET("Fetch/Post/{postId}/LikedBy")
|
||||
fun getLikedBy(@Path("postId") postId: Long): Call<List<ProfileListItemDto>>
|
||||
|
||||
@GET("Feed")
|
||||
fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
|
||||
}
|
||||
@ -1,8 +1,58 @@
|
||||
package com.isolaatti.posting.posts.data.repository
|
||||
|
||||
import com.isolaatti.posting.posts.data.remote.PostsApi
|
||||
import com.google.gson.Gson
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
import com.isolaatti.posting.posts.data.remote.FeedFilterDto
|
||||
import com.isolaatti.posting.posts.data.remote.FeedsApi
|
||||
import com.isolaatti.posting.posts.domain.PostsRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import retrofit2.await
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class PostsRepositoryImpl @Inject constructor(private val postsApi: PostsApi) : PostsRepository {
|
||||
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi) : PostsRepository {
|
||||
override fun getFeed(lastId: Long): Flow<Resource<FeedDto>> = flow {
|
||||
emit(Resource.Loading())
|
||||
try {
|
||||
val result = feedsApi.getChronology(lastId, 20).execute()
|
||||
if(result.isSuccessful) {
|
||||
emit(Resource.Success(result.body()))
|
||||
return@flow
|
||||
}
|
||||
when(result.code()) {
|
||||
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
|
||||
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
|
||||
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
||||
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||
}
|
||||
} catch(_: Exception) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>> = flow {
|
||||
emit(Resource.Loading())
|
||||
try {
|
||||
val gson = Gson()
|
||||
val result = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).execute()
|
||||
if(result.isSuccessful) {
|
||||
emit(Resource.Success(result.body()))
|
||||
return@flow
|
||||
}
|
||||
when(result.code()) {
|
||||
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
|
||||
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
|
||||
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
|
||||
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||
}
|
||||
} catch(_: Exception) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,13 @@
|
||||
package com.isolaatti.posting.posts.domain
|
||||
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
import com.isolaatti.posting.posts.data.remote.FeedFilterDto
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PostsRepository {
|
||||
|
||||
fun getFeed(lastId: Long): Flow<Resource<FeedDto>>
|
||||
|
||||
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>>
|
||||
}
|
||||
@ -11,15 +11,15 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.feed.data.remote.PostDto
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
|
||||
import com.isolaatti.utils.UrlGen.userProfileImage
|
||||
import com.squareup.picasso.Picasso
|
||||
import io.noties.markwon.Markwon
|
||||
|
||||
class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback, private var list: List<PostDto>) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
|
||||
class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback, private var feedDto: FeedDto?) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
|
||||
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) {
|
||||
fun bindView(postDto: PostDto, payloads: List<Any>) {
|
||||
fun bindView(postDto: FeedDto.PostDto, payloads: List<Any>) {
|
||||
|
||||
Log.d("payloads", payloads.count().toString())
|
||||
val likeButton: MaterialButton = itemView.findViewById(R.id.like_button)
|
||||
@ -120,29 +120,24 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
|
||||
|
||||
return FeedViewHolder(view)
|
||||
}
|
||||
override fun getItemCount(): Int = list.size
|
||||
override fun getItemCount(): Int = feedDto?.data?.size ?: 0
|
||||
|
||||
override fun getItemId(position: Int): Long = list[position].post.id
|
||||
|
||||
override fun setHasStableIds(hasStableIds: Boolean) {
|
||||
super.setHasStableIds(true)
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateList(newList: List<PostDto>?, updateEvent: UpdateEvent? = null) {
|
||||
fun updateList(updatedFeed: FeedDto, updateEvent: UpdateEvent? = null) {
|
||||
if(updateEvent == null) {
|
||||
if(newList != null) {
|
||||
list = newList
|
||||
}
|
||||
feedDto = updatedFeed
|
||||
|
||||
notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
val postUpdated = list.find { p -> p.post.id == updateEvent.affectedId } ?: return
|
||||
val position = list.indexOf(postUpdated)
|
||||
val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId } ?: return
|
||||
val position = feedDto?.data?.indexOf(postUpdated) ?: return
|
||||
|
||||
if(newList != null) {
|
||||
list = newList
|
||||
}
|
||||
feedDto = updatedFeed
|
||||
|
||||
when(updateEvent.updateType) {
|
||||
UpdateEvent.UpdateType.POST_LIKED -> {
|
||||
@ -163,6 +158,6 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
|
||||
override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {}
|
||||
|
||||
override fun onBindViewHolder(holder: FeedViewHolder, position: Int, payloads: List<Any>) {
|
||||
holder.bindView(list[position], payloads)
|
||||
holder.bindView(feedDto?.data?.get(position) ?: return, payloads)
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,14 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
|
||||
import com.isolaatti.feed.data.remote.FeedDto
|
||||
import com.isolaatti.feed.domain.repository.FeedRepository
|
||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||
import com.isolaatti.posting.likes.data.remote.LikeDto
|
||||
import com.isolaatti.posting.likes.domain.repository.LikesRepository
|
||||
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
|
||||
import com.isolaatti.posting.posts.domain.PostsRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -19,11 +22,17 @@ import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PostsViewModel @Inject constructor(private val feedRepository: FeedRepository, private val likesRepository: LikesRepository) : ViewModel() {
|
||||
class PostsViewModel @Inject constructor(private val postsRepository: PostsRepository, private val likesRepository: LikesRepository) : ViewModel() {
|
||||
|
||||
private val _posts: MutableLiveData<FeedDto> = MutableLiveData()
|
||||
val posts: LiveData<FeedDto> get() = _posts
|
||||
|
||||
private val _loadingPosts = MutableLiveData(false)
|
||||
val loadingPosts: LiveData<Boolean> get() = _loadingPosts
|
||||
|
||||
private val _errorLoading: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
|
||||
val errorLoading: LiveData<Resource.Error.ErrorType?> get() = _errorLoading
|
||||
|
||||
private val _comments: MutableLiveData<FeedCommentsDto> = MutableLiveData()
|
||||
val comments: LiveData<FeedCommentsDto> get() = _comments
|
||||
|
||||
@ -34,19 +43,19 @@ class PostsViewModel @Inject constructor(private val feedRepository: FeedReposit
|
||||
|
||||
fun getFeed() {
|
||||
viewModelScope.launch {
|
||||
feedRepository.getNextPage(getLastId(), 20).onEach {feedDto ->
|
||||
val temp = _posts.value
|
||||
if(temp != null) {
|
||||
feedDto?.data?.let { it ->
|
||||
temp.data.addAll(it)
|
||||
postsRepository.getFeed(getLastId()).onEach {
|
||||
when(it) {
|
||||
is Resource.Success -> {
|
||||
_loadingPosts.postValue(false)
|
||||
_posts.postValue(posts.value?.concatFeed(it.data) ?: it.data)
|
||||
}
|
||||
temp.let {
|
||||
_posts.postValue(it)
|
||||
is Resource.Loading -> {
|
||||
_loadingPosts.postValue(true)
|
||||
}
|
||||
is Resource.Error -> {
|
||||
_errorLoading.postValue(it.errorType)
|
||||
}
|
||||
} else {
|
||||
_posts.postValue(feedDto)
|
||||
}
|
||||
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.isolaatti.profile.data.remote
|
||||
|
||||
import com.isolaatti.feed.data.remote.FeedDto
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
package com.isolaatti.profile.presentation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.isolaatti.profile.data.remote.UserProfileDto
|
||||
import com.isolaatti.profile.domain.ProfileRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ProfileViewModel @Inject constructor(private val profileRepository: ProfileRepository) : ViewModel() {
|
||||
private val _profile = MutableLiveData<UserProfileDto>()
|
||||
val profile: LiveData<UserProfileDto> get() = _profile
|
||||
|
||||
fun getProfile(profileId: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,21 @@ package com.isolaatti.profile.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.viewpager.widget.PagerAdapter
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.adapter.FragmentViewHolder
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.databinding.ActivityProfileBinding
|
||||
import com.isolaatti.profile.data.remote.UserProfileDto
|
||||
import com.isolaatti.profile.presentation.ProfileViewModel
|
||||
import com.isolaatti.utils.UrlGen
|
||||
import com.squareup.picasso.Picasso
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@ -38,19 +44,24 @@ class ProfileActivity : AppCompatActivity() {
|
||||
|
||||
|
||||
lateinit var viewBinding: ActivityProfileBinding
|
||||
private val viewModel: ProfileViewModel by viewModels()
|
||||
|
||||
private val profileObserver = Observer<UserProfileDto> { profile ->
|
||||
Picasso.get()
|
||||
.load(UrlGen.userProfileImage(profile.id))
|
||||
.into(viewBinding.profileImageView)
|
||||
|
||||
viewBinding.textViewUsername.text = profile.name
|
||||
viewBinding.textViewDescription.text = profile.descriptionText
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewBinding = ActivityProfileBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(viewBinding.root)
|
||||
|
||||
Picasso.get().load("https://isolaatti.com/api/images/image/63a2a6c5270ecc2be2512799?mode=reduced").into(viewBinding.profileImageView)
|
||||
viewBinding.textViewUsername.text = "Erik Everardo"
|
||||
viewBinding.textViewDescription.text = "Hola"
|
||||
viewBinding.profileViewPager2.adapter = ViewPagerAdapter(this)
|
||||
viewBinding.topAppBar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
finish()
|
||||
}
|
||||
|
||||
TabLayoutMediator(viewBinding.profileTabLayout, viewBinding.profileViewPager2) {tab, position ->
|
||||
@ -66,5 +77,11 @@ class ProfileActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}.attach()
|
||||
|
||||
viewModel.profile.observe(this, profileObserver)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_USER_ID = "user_id"
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
package com.isolaatti.utils
|
||||
|
||||
class Resource<T> {
|
||||
inner class Success(data: T)
|
||||
|
||||
inner class Loading()
|
||||
|
||||
inner class Error
|
||||
abstract class Resource<T> {
|
||||
class Success<T>(val data: T?): Resource<T>()
|
||||
class Loading<T>: Resource<T>()
|
||||
class Error<T>(val errorType: ErrorType? = null): Resource<T>() {
|
||||
enum class ErrorType {
|
||||
NetworkError, AuthError, NotFoundError, ServerError, OtherError
|
||||
}
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/drawable/baseline_keyboard_voice_24.xml
Normal file
5
app/src/main/res/drawable/baseline_keyboard_voice_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,15c1.66,0 2.99,-1.34 2.99,-3L15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6v6c0,1.66 1.34,3 3,3zM17.3,12c0,3 -2.54,5.1 -5.3,5.1S6.7,15 6.7,12L5,12c0,3.42 2.72,6.23 6,6.72L11,22h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_mic_24.xml
Normal file
5
app/src/main/res/drawable/baseline_mic_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/baseline_post_add_24.xml
Normal file
9
app/src/main/res/drawable/baseline_post_add_24.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,19.22H5V7h7V5H5C3.9,5 3,5.9 3,7v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-7h-2V19.22z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,2h-2v3h-3c0.01,0.01 0,2 0,2h3v2.99c0.01,0.01 2,0 2,0V7h3V5h-3V2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,9h8v2h-8z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,12l0,2l8,0l0,-2l-3,0z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,15h8v2h-8z"/>
|
||||
</vector>
|
||||
@ -17,7 +17,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/home_drawer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:context=".feed.ui.FeedFragment">
|
||||
tools:context=".home.FeedFragment">
|
||||
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@
|
||||
android:layout_height="match_parent"
|
||||
app:title="@string/app_name"
|
||||
app:titleCentered="true"
|
||||
app:menu="@menu/new_menu"
|
||||
tools:layout_conversion_absoluteHeight="64dp"
|
||||
tools:layout_conversion_absoluteWidth="531dp"
|
||||
tools:layout_editor_absoluteX="360dp"
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:translationZ="10dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
@ -89,4 +101,5 @@
|
||||
android:text="@string/sign_up"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
@ -6,47 +6,49 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/topAppBar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/topAppBar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_app_bar"
|
||||
style="@style/Theme.Isolaatti"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:titleCentered="true"
|
||||
style="@style/Theme.Isolaatti"
|
||||
app:navigationIcon="@drawable/baseline_arrow_back_24"
|
||||
app:title="Profile"/>
|
||||
|
||||
app:title="Profile"
|
||||
app:titleCentered="true" />
|
||||
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/topAppBar_layout">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/profile_image_view"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
@ -54,30 +56,76 @@
|
||||
android:id="@+id/text_view_username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_weight="1"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_image_view"
|
||||
tools:text="Erik Cavazos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:maxLines="4"
|
||||
android:textAlignment="center"
|
||||
tools:text="Hi there, I am software developer!"/>
|
||||
</LinearLayout>
|
||||
app:layout_constraintTop_toBottomOf="@id/text_view_username"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="Hi there, I am software developer!" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_following_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_view_description"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="Following this profile" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
|
||||
app:layout_constraintEnd_toStartOf="@id/profile_options_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_view_following_state" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/profile_options_button"
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:icon="@drawable/baseline_more_horiz_24"
|
||||
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_view_following_state"/>
|
||||
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/profile_tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabContentStart="56dp"
|
||||
app:tabMode="fixed"
|
||||
app:tabContentStart="56dp"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_options_button"/>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/profile_view_pager2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_tab_layout"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
|
||||
35
app/src/main/res/layout/audio_recorder_activity.xml
Normal file
35
app/src/main/res/layout/audio_recorder_activity.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/topAppBar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/topAppBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/Theme.Isolaatti"
|
||||
app:navigationIcon="@drawable/baseline_close_24"
|
||||
app:navigationIconTint="@color/on_surface"
|
||||
app:title="@string/audio_recorder"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/topAppBar_layout" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
6
app/src/main/res/layout/audio_recorder_main_fragment.xml
Normal file
6
app/src/main/res/layout/audio_recorder_main_fragment.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -58,7 +58,7 @@
|
||||
app:boxCornerRadiusTopEnd="20dp"
|
||||
app:boxCornerRadiusTopStart="20dp"
|
||||
|
||||
app:startIconDrawable="@drawable/baseline_open_in_full_24"
|
||||
app:startIconDrawable="@drawable/baseline_keyboard_voice_24"
|
||||
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/submitCommentButton"
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".feed.ui.FeedFragment">
|
||||
tools:context=".home.FeedFragment">
|
||||
|
||||
|
||||
<FrameLayout
|
||||
@ -29,7 +29,8 @@
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/topAppBar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/topAppBar"
|
||||
@ -38,26 +39,23 @@
|
||||
android:theme="@style/Theme.Isolaatti"
|
||||
app:navigationIcon="@drawable/baseline_menu_24"
|
||||
app:navigationIconTint="@color/on_surface"
|
||||
app:menu="@menu/new_menu"
|
||||
app:title="@string/app_name"
|
||||
app:titleCentered="true" />
|
||||
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/feed_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/floating_action_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription=""
|
||||
app:srcCompat="@drawable/baseline_add_24" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="2000dp"
|
||||
android:text="@string/images"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
34
app/src/main/res/layout/fragment_search.xml
Normal file
34
app/src/main/res/layout/fragment_search.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.search.SearchBar$ScrollingViewBehavior">
|
||||
<!-- Screen content goes here. -->
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.search.SearchBar
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/searchbar_hint" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<com.google.android.material.search.SearchView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/searchbar_hint"
|
||||
app:layout_anchor="@id/search_bar">
|
||||
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
|
||||
</com.google.android.material.search.SearchView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@ -3,7 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:icon="@drawable/baseline_notifications_24"
|
||||
android:title="Notifications"
|
||||
app:showAsAction="always" />
|
||||
android:icon="@drawable/baseline_add_24"
|
||||
app:showAsAction="always"
|
||||
android:title="@string/new_post" />
|
||||
</menu>
|
||||
13
app/src/main/res/menu/new_menu.xml
Normal file
13
app/src/main/res/menu/new_menu.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/menu_item_new_discussion"
|
||||
android:title="@string/discussion"
|
||||
android:icon="@drawable/baseline_post_add_24"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item android:id="@+id/menu_item_new_audio"
|
||||
android:title="@string/add_audio"
|
||||
android:icon="@drawable/baseline_mic_24"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
14
app/src/main/res/navigation/audio_recorder_navigation.xml
Normal file
14
app/src/main/res/navigation/audio_recorder_navigation.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/audio_recorder_navigation"
|
||||
app:startDestination="@id/audioRecorderMainFragment">
|
||||
<fragment
|
||||
android:id="@+id/audioRecorderMainFragment"
|
||||
android:name="com.isolaatti.audio.recorder.ui.AudioRecorderMainFragment"
|
||||
android:label="AudioRecorderMainFragment" />
|
||||
<fragment
|
||||
android:id="@+id/audioDraftsFragment"
|
||||
android:name="com.isolaatti.audio.recorder.ui.AudioDraftsFragment"
|
||||
android:label="AudioDraftsFragment" />
|
||||
</navigation>
|
||||
@ -7,12 +7,28 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/feedFragment"
|
||||
android:name="com.isolaatti.feed.ui.FeedFragment"
|
||||
android:name="com.isolaatti.home.FeedFragment"
|
||||
android:label="fragment_feed"
|
||||
tools:layout="@layout/fragment_feed" />
|
||||
tools:layout="@layout/fragment_feed" >
|
||||
<action
|
||||
android:id="@+id/action_feedFragment_to_profileActivity"
|
||||
app:destination="@id/profileActivity" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/notificationsFragment"
|
||||
android:name="com.isolaatti.home.notifications.ui.NotificationsFragment"
|
||||
android:label="fragment_notifications"
|
||||
tools:layout="@layout/fragment_notifications" />
|
||||
tools:layout="@layout/fragment_notifications" >
|
||||
<action
|
||||
android:id="@+id/action_notificationsFragment_to_profileActivity"
|
||||
app:destination="@id/profileActivity" />
|
||||
</fragment>
|
||||
<activity
|
||||
android:id="@+id/profileActivity"
|
||||
android:name="com.isolaatti.profile.ui.ProfileActivity"
|
||||
android:label="ProfileActivity" />
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
android:name="com.isolaatti.home.search.presentation.SearchFragment"
|
||||
android:label="@string/search" />
|
||||
</navigation>
|
||||
@ -18,9 +18,27 @@
|
||||
<string name="report">Report</string>
|
||||
<string name="close">Close</string>
|
||||
|
||||
<!-- Common -->
|
||||
<string name="accept">Accept</string>
|
||||
<string name="no">No</string>
|
||||
|
||||
<!-- error messages-->
|
||||
<string name="need_reauth_message">Your session is not valid, please enter your credentials.</string>
|
||||
<string name="network_error">Network error occurred, it maybe is on our side. Please try again later</string>
|
||||
<string name="server_error">An error on our side has occurred.</string>
|
||||
|
||||
<!--Post options -->
|
||||
<string name="post_options_title">Discussion options</string>
|
||||
<string name="home">Home</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="search">Search</string>
|
||||
|
||||
<!-- Audio recorder -->
|
||||
<string name="audio_recorder">Record audio</string>
|
||||
<string name="account_not_found">Could not find any account that matches %s</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="wrong_password">Could not sign in, password does not match</string>
|
||||
<string name="new_post">New</string>
|
||||
<string name="discussion">Discussion</string>
|
||||
<string name="add_audio">New audio</string>
|
||||
</resources>
|
||||
@ -1,7 +1,13 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.2' apply false
|
||||
id 'com.android.library' version '8.0.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
|
||||
id 'com.google.dagger.hilt.android' version '2.44' apply false
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user